Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
.col-point-id, .col-pointtrigger-id {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.trigger-event-group-header {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#triggerEvents .trigger-event-row {
|
||||
padding: 20px;
|
||||
margin-bottom: 0 !important;
|
||||
border: 1px solid var(--border-subtle);
|
||||
position: relative;
|
||||
transition: var(--transition-all-productive);
|
||||
}
|
||||
|
||||
#triggerEvents .trigger-event-row .event-label {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#triggerEvents .trigger-event-row .event-descr {
|
||||
font-size: 0.9em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#triggerEvents .trigger-event-row:hover {
|
||||
background-color: var(--layer-hover);
|
||||
}
|
||||
|
||||
#triggerEvents .trigger-event-row.bg-danger:hover {
|
||||
background-color: var(--support-error-inverse);
|
||||
}
|
||||
|
||||
#triggerEvents .form-buttons {
|
||||
float: right;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 20px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.col-pointtrigger-color {
|
||||
width: 50px;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
//PointBundle
|
||||
Mautic.pointOnLoad = function (container) {
|
||||
if (mQuery(container + ' #list-search').length) {
|
||||
Mautic.activateSearchAutocomplete('list-search', 'point');
|
||||
}
|
||||
};
|
||||
|
||||
Mautic.pointTriggerOnLoad = function (container) {
|
||||
if (mQuery(container + ' #list-search').length) {
|
||||
Mautic.activateSearchAutocomplete('list-search', 'point.trigger');
|
||||
}
|
||||
|
||||
if (mQuery('#triggerEvents')) {
|
||||
//make the fields sortable
|
||||
mQuery('#triggerEvents').sortable({
|
||||
items: '.trigger-event-row',
|
||||
handle: '.reorder-handle',
|
||||
stop: function(i) {
|
||||
mQuery.ajax({
|
||||
type: "POST",
|
||||
url: mauticAjaxUrl + "?action=point:reorderTriggerEvents",
|
||||
data: mQuery('#triggerEvents').sortable("serialize") + "&triggerId=" + mQuery('#pointtrigger_sessionId').val()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
mQuery('#triggerEvents .trigger-event-row').on('mouseover.triggerevents', function() {
|
||||
mQuery(this).find('.form-buttons').removeClass('hide');
|
||||
}).on('mouseout.triggerevents', function() {
|
||||
mQuery(this).find('.form-buttons').addClass('hide');
|
||||
}).on('dblclick.triggerevents', function(event) {
|
||||
event.preventDefault();
|
||||
mQuery(this).find('.btn-edit').first().click();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Mautic.pointTriggerEventOnLoad = function (container, response) {
|
||||
//new action created so append it to the form
|
||||
if (response.eventHtml) {
|
||||
var newHtml = response.eventHtml;
|
||||
var eventId = '#triggerEvent_' + response.eventId;
|
||||
if (mQuery(eventId).length) {
|
||||
//replace content
|
||||
mQuery(eventId).replaceWith(newHtml);
|
||||
var newField = false;
|
||||
} else {
|
||||
//append content
|
||||
mQuery(newHtml).appendTo('#triggerEvents');
|
||||
var newField = true;
|
||||
}
|
||||
|
||||
//initialize tooltips
|
||||
mQuery(eventId + " *[data-toggle='tooltip']").tooltip({html: true});
|
||||
|
||||
//activate new stuff
|
||||
mQuery(eventId + " a[data-toggle='ajax']").click(function (event) {
|
||||
event.preventDefault();
|
||||
return Mautic.ajaxifyLink(this, event);
|
||||
});
|
||||
|
||||
//initialize ajax'd modals
|
||||
mQuery(eventId + " a[data-toggle='ajaxmodal']").on('click.ajaxmodal', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
Mautic.ajaxifyModal(this, event);
|
||||
});
|
||||
|
||||
mQuery('#triggerEvents .trigger-event-row').off(".triggerevents");
|
||||
mQuery('#triggerEvents .trigger-event-row').on('mouseover.triggerevents', function() {
|
||||
mQuery(this).find('.form-buttons').removeClass('hide');
|
||||
}).on('mouseout.triggerevents', function() {
|
||||
mQuery(this).find('.form-buttons').addClass('hide');
|
||||
}).on('dblclick.triggerevents', function(event) {
|
||||
event.preventDefault();
|
||||
mQuery(this).find('.btn-edit').first().click();
|
||||
});
|
||||
|
||||
//show events panel
|
||||
if (!mQuery('#events-panel').hasClass('in')) {
|
||||
mQuery('a[href="#events-panel"]').trigger('click');
|
||||
}
|
||||
|
||||
if (mQuery('#triggerEventPlaceholder').length) {
|
||||
mQuery('#triggerEventPlaceholder').remove();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Mautic.getPointActionPropertiesForm = function(actionType) {
|
||||
Mautic.activateLabelLoadingIndicator('point_type');
|
||||
|
||||
var query = "action=point:getActionForm&actionType=" + actionType;
|
||||
mQuery.ajax({
|
||||
url: mauticAjaxUrl,
|
||||
type: "POST",
|
||||
data: query,
|
||||
dataType: "json",
|
||||
success: function (response) {
|
||||
if (typeof response.html != 'undefined') {
|
||||
mQuery('#pointActionProperties').html(response.html);
|
||||
Mautic.onPageLoad('#pointActionProperties', response);
|
||||
}
|
||||
},
|
||||
error: function (request, textStatus, errorThrown) {
|
||||
Mautic.processAjaxError(request, textStatus, errorThrown);
|
||||
},
|
||||
complete: function() {
|
||||
Mautic.removeLabelLoadingIndicator();
|
||||
}
|
||||
});
|
||||
};
|
||||
Mautic.EnablesOption = function (urlActionProperty) {
|
||||
if (urlActionProperty === 'point_properties_returns_within' && mQuery('#point_properties_returns_within').val() > 0) {
|
||||
mQuery('#point_properties_returns_after').val(0);
|
||||
} else {
|
||||
if (urlActionProperty === 'point_properties_returns_after' && mQuery('#point_properties_returns_after').val() > 0) {
|
||||
mQuery('#point_properties_returns_within').val(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'routes' => [
|
||||
'main' => [
|
||||
'mautic_pointtriggerevent_action' => [
|
||||
'path' => '/points/triggers/events/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\PointBundle\Controller\TriggerEventController::executeAction',
|
||||
],
|
||||
'mautic_pointtrigger_index' => [
|
||||
'path' => '/points/triggers/{page}',
|
||||
'controller' => 'Mautic\PointBundle\Controller\TriggerController::indexAction',
|
||||
],
|
||||
'mautic_pointtrigger_action' => [
|
||||
'path' => '/points/triggers/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\PointBundle\Controller\TriggerController::executeAction',
|
||||
],
|
||||
'mautic_point.group_index' => [
|
||||
'path' => '/points/groups/{page}',
|
||||
'controller' => 'Mautic\PointBundle\Controller\GroupController::indexAction',
|
||||
],
|
||||
'mautic_point.group_action' => [
|
||||
'path' => '/points/groups/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\PointBundle\Controller\GroupController::executeAction',
|
||||
],
|
||||
'mautic_point_index' => [
|
||||
'path' => '/points/{page}',
|
||||
'controller' => 'Mautic\PointBundle\Controller\PointController::indexAction',
|
||||
],
|
||||
'mautic_point_action' => [
|
||||
'path' => '/points/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\PointBundle\Controller\PointController::executeAction',
|
||||
],
|
||||
],
|
||||
'api' => [
|
||||
'mautic_api_pointactionsstandard' => [
|
||||
'standard_entity' => true,
|
||||
'name' => 'points',
|
||||
'path' => '/points',
|
||||
'controller' => Mautic\PointBundle\Controller\Api\PointApiController::class,
|
||||
],
|
||||
'mautic_api_getpointactiontypes' => [
|
||||
'path' => '/points/actions/types',
|
||||
'controller' => 'Mautic\PointBundle\Controller\Api\PointApiController::getPointActionTypesAction',
|
||||
],
|
||||
'mautic_api_pointtriggersstandard' => [
|
||||
'standard_entity' => true,
|
||||
'name' => 'triggers',
|
||||
'path' => '/points/triggers',
|
||||
'controller' => Mautic\PointBundle\Controller\Api\TriggerApiController::class,
|
||||
],
|
||||
'mautic_api_getpointtriggereventtypes' => [
|
||||
'path' => '/points/triggers/events/types',
|
||||
'controller' => 'Mautic\PointBundle\Controller\Api\TriggerApiController::getPointTriggerEventTypesAction',
|
||||
],
|
||||
'mautic_api_pointtriggerdeleteevents' => [
|
||||
'path' => '/points/triggers/{triggerId}/events/delete',
|
||||
'controller' => 'Mautic\PointBundle\Controller\Api\TriggerApiController::deletePointTriggerEventsAction',
|
||||
'method' => 'DELETE',
|
||||
],
|
||||
'mautic_api_adjustcontactpoints' => [
|
||||
'path' => '/contacts/{leadId}/points/{operator}/{delta}',
|
||||
'controller' => 'Mautic\PointBundle\Controller\Api\PointApiController::adjustPointsAction',
|
||||
'method' => 'POST',
|
||||
],
|
||||
'mautic_api_pointgroupsstandard' => [
|
||||
'standard_entity' => true,
|
||||
'name' => 'pointGroups',
|
||||
'path' => '/points/groups',
|
||||
'controller' => Mautic\PointBundle\Controller\Api\PointGroupsApiController::class,
|
||||
],
|
||||
'mautic_api_getcontactpointgroups' => [
|
||||
'path' => '/contacts/{contactId}/points/groups',
|
||||
'controller' => 'Mautic\PointBundle\Controller\Api\PointGroupsApiController::getContactPointGroupsAction',
|
||||
],
|
||||
'mautic_api_getcontactpointgroup' => [
|
||||
'path' => '/contacts/{contactId}/points/groups/{groupId}',
|
||||
'controller' => 'Mautic\PointBundle\Controller\Api\PointGroupsApiController::getContactPointGroupAction',
|
||||
],
|
||||
'mautic_api_adjustcontactgrouppoints' => [
|
||||
'path' => '/contacts/{contactId}/points/groups/{groupId}/{operator}/{value}',
|
||||
'controller' => 'Mautic\PointBundle\Controller\Api\PointGroupsApiController::adjustGroupPointsAction',
|
||||
'method' => 'POST',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'menu' => [
|
||||
'main' => [
|
||||
'mautic.points.menu.root' => [
|
||||
'id' => 'mautic_points_root',
|
||||
'iconClass' => 'ri-coins-fill',
|
||||
'access' => ['point:points:view', 'point:triggers:view', 'point:groups:view'],
|
||||
'priority' => 30,
|
||||
'children' => [
|
||||
'mautic.point.menu.index' => [
|
||||
'route' => 'mautic_point_index',
|
||||
'access' => 'point:points:view',
|
||||
],
|
||||
'mautic.point.trigger.menu.index' => [
|
||||
'route' => 'mautic_pointtrigger_index',
|
||||
'access' => 'point:triggers:view',
|
||||
],
|
||||
'mautic.point.group.menu.index' => [
|
||||
'route' => 'mautic_point.group_index',
|
||||
'access' => 'point:groups:view',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'categories' => [
|
||||
'point' => [
|
||||
'class' => Mautic\PointBundle\Entity\Point::class,
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mautic\CoreBundle\DependencyInjection\MauticCoreExtension;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return function (ContainerConfigurator $configurator): void {
|
||||
$services = $configurator->services()
|
||||
->defaults()
|
||||
->autowire()
|
||||
->autoconfigure()
|
||||
->public();
|
||||
|
||||
$excludes = [
|
||||
];
|
||||
|
||||
$services->load('Mautic\\PointBundle\\', '../')
|
||||
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
|
||||
|
||||
$services->load('Mautic\\PointBundle\\Entity\\', '../Entity/*Repository.php')
|
||||
->tag(Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass::REPOSITORY_SERVICE_TAG);
|
||||
|
||||
$services->alias('mautic.point.model.point', Mautic\PointBundle\Model\PointModel::class);
|
||||
$services->alias('mautic.point.model.triggerevent', Mautic\PointBundle\Model\TriggerEventModel::class);
|
||||
$services->alias('mautic.point.model.trigger', Mautic\PointBundle\Model\TriggerModel::class);
|
||||
$services->alias('mautic.point.model.group', Mautic\PointBundle\Model\PointGroupModel::class);
|
||||
$services->alias('mautic.point.repository.lead_point_log', Mautic\PointBundle\Entity\LeadPointLogRepository::class);
|
||||
$services->alias('mautic.point.repository.lead_trigger_log', Mautic\PointBundle\Entity\LeadTriggerLogRepository::class);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\AjaxController as CommonAjaxController;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\PointBundle\Form\Type\PointActionType;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class AjaxController extends CommonAjaxController
|
||||
{
|
||||
public function reorderTriggerEventsAction(Request $request): \Symfony\Component\HttpFoundation\JsonResponse
|
||||
{
|
||||
$dataArray = ['success' => 0];
|
||||
$session = $request->getSession();
|
||||
$triggerId = InputHelper::clean($request->request->get('triggerId'));
|
||||
$sessionName = 'mautic.point.'.$triggerId.'.triggerevents.modified';
|
||||
$order = InputHelper::clean($request->request->get('triggerEvent'));
|
||||
$components = $session->get($sessionName);
|
||||
|
||||
if (!empty($order) && !empty($components)) {
|
||||
$components = array_replace(array_flip($order), $components);
|
||||
$session->set($sessionName, $components);
|
||||
$dataArray['success'] = 1;
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
public function getActionFormAction(Request $request, FormFactoryInterface $formFactory): \Symfony\Component\HttpFoundation\JsonResponse
|
||||
{
|
||||
$type = InputHelper::clean($request->request->get('actionType'));
|
||||
$dataArray = [
|
||||
'success' => 0,
|
||||
'html' => '',
|
||||
];
|
||||
|
||||
if (!empty($type)) {
|
||||
// get the HTML for the form
|
||||
/** @var \Mautic\PointBundle\Model\PointModel $model */
|
||||
$model = $this->getModel('point');
|
||||
$actions = $model->getPointActions();
|
||||
|
||||
if (isset($actions['actions'][$type])) {
|
||||
$themes = ['@MauticPoint/FormTheme/Action/_pointaction_properties_row.html.twig'];
|
||||
|
||||
if (!empty($actions['actions'][$type]['formTheme'])) {
|
||||
$themes[] = $actions['actions'][$type]['formTheme'];
|
||||
}
|
||||
|
||||
$formType = (!empty($actions['actions'][$type]['formType'])) ? $actions['actions'][$type]['formType'] : null;
|
||||
$formTypeOptions = (!empty($actions['actions'][$type]['formTypeOptions'])) ? $actions['actions'][$type]['formTypeOptions'] : [];
|
||||
$form = $formFactory->create(PointActionType::class, [], ['formType' => $formType, 'formTypeOptions' => $formTypeOptions]);
|
||||
$html = $this->renderView('@MauticPoint/Point/actionform.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'formThemes' => $themes,
|
||||
]);
|
||||
|
||||
// replace pointaction with point
|
||||
$html = str_replace('pointaction', 'point', $html);
|
||||
$dataArray['html'] = $html;
|
||||
$dataArray['success'] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Controller\LeadAccessTrait;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\PointBundle\Entity\Point;
|
||||
use Mautic\PointBundle\Model\PointModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<Point>
|
||||
*/
|
||||
class PointApiController extends CommonApiController
|
||||
{
|
||||
use LeadAccessTrait;
|
||||
|
||||
protected LeadModel $leadModel;
|
||||
|
||||
/**
|
||||
* @var PointModel|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
public function __construct(CorePermissions $security, Translator $translator, EntityResultHelper $entityResultHelper, RouterInterface $router, FormFactoryInterface $formFactory, AppVersion $appVersion, RequestStack $requestStack, ManagerRegistry $doctrine, ModelFactory $modelFactory, EventDispatcherInterface $dispatcher, CoreParametersHelper $coreParametersHelper)
|
||||
{
|
||||
$leadModel = $modelFactory->getModel('lead');
|
||||
\assert($leadModel instanceof LeadModel);
|
||||
|
||||
$pointModel = $modelFactory->getModel('point');
|
||||
\assert($pointModel instanceof PointModel);
|
||||
|
||||
$this->model = $pointModel;
|
||||
$this->leadModel = $leadModel;
|
||||
$this->entityClass = Point::class;
|
||||
$this->entityNameOne = 'point';
|
||||
$this->entityNameMulti = 'points';
|
||||
$this->serializerGroups = ['pointDetails', 'categoryList', 'publishDetails'];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array of available point action types.
|
||||
*/
|
||||
public function getPointActionTypesAction()
|
||||
{
|
||||
if (!$this->security->isGranted([$this->permissionBase.':view', $this->permissionBase.':viewown'])) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$actionTypes = $this->model->getPointActions();
|
||||
$view = $this->view(['pointActionTypes' => $actionTypes['list']]);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtract points from a lead.
|
||||
*
|
||||
* @param int $leadId
|
||||
* @param string $operator
|
||||
* @param int $delta
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function adjustPointsAction(Request $request, IpLookupHelper $ipLookupHelper, $leadId, $operator, $delta)
|
||||
{
|
||||
$lead = $this->checkLeadAccess($leadId, 'edit');
|
||||
if ($lead instanceof Response) {
|
||||
return $lead;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->logApiPointChange($request, $ipLookupHelper, $lead, $delta, $operator);
|
||||
} catch (\Exception $e) {
|
||||
return $this->returnError($e->getMessage(), Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
return $this->handleView($this->view(['success' => 1], Response::HTTP_OK));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the lead points change.
|
||||
*
|
||||
* @param int $delta
|
||||
*/
|
||||
protected function logApiPointChange(Request $request, IpLookupHelper $ipLookupHelper, $lead, $delta, $operator)
|
||||
{
|
||||
$trans = $this->translator;
|
||||
$ip = $ipLookupHelper->getIpAddress();
|
||||
$eventName = InputHelper::clean($request->request->get('eventName', $trans->trans('mautic.lead.lead.submitaction.operator_'.$operator)));
|
||||
$actionName = InputHelper::clean($request->request->get('actionName', $trans->trans('mautic.lead.event.api')));
|
||||
|
||||
$lead->adjustPoints($delta, $operator);
|
||||
$lead->addPointsChangeLogEntry('API', $eventName, $actionName, $delta, $ip);
|
||||
$this->leadModel->saveEntity($lead, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\PointBundle\Entity\Group;
|
||||
use Mautic\PointBundle\Model\PointGroupModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<Group>
|
||||
*/
|
||||
class PointGroupsApiController extends CommonApiController
|
||||
{
|
||||
/**
|
||||
* @var PointGroupModel
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
public function __construct(CorePermissions $security, Translator $translator, EntityResultHelper $entityResultHelper, RouterInterface $router, FormFactoryInterface $formFactory, AppVersion $appVersion, RequestStack $requestStack, ManagerRegistry $doctrine, ModelFactory $modelFactory, EventDispatcherInterface $dispatcher, CoreParametersHelper $coreParametersHelper, PointGroupModel $pointGroupModel, private LeadModel $leadModel)
|
||||
{
|
||||
$this->model = $pointGroupModel;
|
||||
$this->entityClass = Group::class;
|
||||
$this->entityNameOne = 'pointGroup';
|
||||
$this->entityNameMulti = 'pointGroups';
|
||||
$this->serializerGroups = ['pointGroupDetails', 'pointGroupList', 'publishDetails'];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
public function getContactPointGroupsAction(int $contactId): Response
|
||||
{
|
||||
$contact = $this->leadModel->getEntity($contactId);
|
||||
|
||||
if (null === $contact) {
|
||||
return $this->notFound($this->translator->trans('mautic.lead.event.api.lead.not.found'));
|
||||
}
|
||||
|
||||
if (!$this->checkEntityAccess($contact)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$groupScores = $contact->getGroupScores();
|
||||
$view = $this->view(
|
||||
[
|
||||
'total' => count($groupScores),
|
||||
'groupScores' => $groupScores,
|
||||
],
|
||||
Response::HTTP_OK
|
||||
);
|
||||
|
||||
$context = $view->getContext()->setGroups(['groupContactScoreDetails', 'pointGroupDetails']);
|
||||
$view->setContext($context);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
public function getContactPointGroupAction(int $contactId, int $groupId): Response
|
||||
{
|
||||
$contact = $this->leadModel->getEntity($contactId);
|
||||
|
||||
if (null === $contact) {
|
||||
return $this->notFound($this->translator->trans('mautic.lead.event.api.lead.not.found'));
|
||||
}
|
||||
|
||||
if (!$this->checkEntityAccess($contact)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$pointGroup = $this->model->getEntity($groupId);
|
||||
if (null === $pointGroup) {
|
||||
return $this->notFound($this->translator->trans('mautic.lead.event.api.point.group.not.found'));
|
||||
}
|
||||
|
||||
$groupScore = $contact->getGroupScore($pointGroup);
|
||||
$view = $this->view(
|
||||
[
|
||||
'groupScore' => $groupScore,
|
||||
],
|
||||
Response::HTTP_OK
|
||||
);
|
||||
|
||||
$context = $view->getContext()->setGroups(['groupContactScoreDetails', 'pointGroupDetails']);
|
||||
$view->setContext($context);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
public function adjustGroupPointsAction(Request $request, IpLookupHelper $ipLookupHelper, int $contactId, int $groupId, string $operator, int $value): Response
|
||||
{
|
||||
$contact = $this->leadModel->getEntity($contactId);
|
||||
|
||||
if (null === $contact) {
|
||||
return $this->notFound($this->translator->trans('mautic.lead.event.api.lead.not.found'));
|
||||
}
|
||||
|
||||
if (!$this->checkEntityAccess($contact)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$pointGroup = $this->model->getEntity($groupId);
|
||||
if (null === $pointGroup) {
|
||||
return $this->notFound($this->translator->trans('mautic.lead.event.api.point.group.not.found'));
|
||||
}
|
||||
|
||||
if (!PointGroupModel::isAllowedPointOperation($operator)) {
|
||||
return $this->badRequest($this->translator->trans('mautic.lead.event.api.operation.not.allowed'));
|
||||
}
|
||||
|
||||
$oldScore = $contact->getGroupScore($pointGroup)?->getScore();
|
||||
$contact = $this->model->adjustPoints($contact, $pointGroup, $value, $operator);
|
||||
$newScore = $contact->getGroupScore($pointGroup)->getScore();
|
||||
$delta = $newScore - ($oldScore ?? 0);
|
||||
|
||||
$eventName = InputHelper::clean($request->request->get('eventName', $this->translator->trans('mautic.point.event.manual_change')));
|
||||
$actionName = InputHelper::clean($request->request->get('actionName', $this->translator->trans('mautic.lead.event.api')));
|
||||
$contact->addPointsChangeLogEntry(
|
||||
type: 'API',
|
||||
name: $eventName,
|
||||
action: $actionName,
|
||||
pointChanges: $delta,
|
||||
ip: $ipLookupHelper->getIpAddress(),
|
||||
group: $pointGroup
|
||||
);
|
||||
$this->leadModel->saveEntity($contact, false);
|
||||
|
||||
$view = $this->view(['groupScore' => $contact->getGroupScore($pointGroup)], Response::HTTP_OK);
|
||||
$context = $view->getContext()->setGroups(['groupContactScoreDetails', 'pointGroupDetails']);
|
||||
$view->setContext($context);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\PointBundle\Entity\Trigger;
|
||||
use Mautic\PointBundle\Model\TriggerEventModel;
|
||||
use Mautic\PointBundle\Model\TriggerModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<Trigger>
|
||||
*/
|
||||
class TriggerApiController extends CommonApiController
|
||||
{
|
||||
/**
|
||||
* @var TriggerModel|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
public function __construct(
|
||||
CorePermissions $security,
|
||||
Translator $translator,
|
||||
EntityResultHelper $entityResultHelper,
|
||||
RouterInterface $router,
|
||||
FormFactoryInterface $formFactory,
|
||||
AppVersion $appVersion,
|
||||
private ?RequestStack $requestStack,
|
||||
ManagerRegistry $doctrine,
|
||||
ModelFactory $modelFactory,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
$triggerModel = $modelFactory->getModel('point.trigger');
|
||||
\assert($triggerModel instanceof TriggerModel);
|
||||
|
||||
$this->model = $triggerModel;
|
||||
$this->entityClass = Trigger::class;
|
||||
$this->entityNameOne = 'trigger';
|
||||
$this->entityNameMulti = 'triggers';
|
||||
$this->serializerGroups = ['triggerDetails', 'categoryList', 'publishDetails'];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit')
|
||||
{
|
||||
$method = $this->requestStack->getCurrentRequest()->getMethod();
|
||||
$triggerEventModel = $this->getModel('point.triggerevent');
|
||||
$isNew = false;
|
||||
|
||||
// Set timestamps
|
||||
$this->model->setTimestamps($entity, true, false);
|
||||
|
||||
if (!$entity->getId()) {
|
||||
$isNew = true;
|
||||
|
||||
// Save the entitz first to get the ID.
|
||||
// Using the repository function to not trigger the listeners twice.
|
||||
$this->model->getRepository()->saveEntity($entity);
|
||||
}
|
||||
|
||||
$requestTriggerIds = [];
|
||||
$currentEvents = $entity->getEvents();
|
||||
|
||||
// Add events from the request
|
||||
if (!empty($parameters['events']) && is_array($parameters['events'])) {
|
||||
foreach ($parameters['events'] as &$eventParams) {
|
||||
if (empty($eventParams['id'])) {
|
||||
// Create an unique ID if not set - the following code requires one
|
||||
$eventParams['id'] = 'new'.hash('sha1', uniqid(mt_rand()));
|
||||
$triggerEventEntity = $triggerEventModel->getEntity();
|
||||
} else {
|
||||
$triggerEventEntity = $triggerEventModel->getEntity($eventParams['id']);
|
||||
$requestTriggerIds[] = $eventParams['id'];
|
||||
}
|
||||
|
||||
$triggerEventForm = $this->createTriggerEventEntityForm($triggerEventEntity);
|
||||
$triggerEventForm->submit($eventParams, 'PATCH' !== $method);
|
||||
|
||||
if (!($triggerEventForm->isSubmitted() && $triggerEventForm->isValid())) {
|
||||
$formErrors = $this->getFormErrorMessages($triggerEventForm);
|
||||
$msg = $this->getFormErrorMessage($formErrors);
|
||||
|
||||
return $this->returnError('Trigger events: '.$msg, Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
$this->model->setEvents($entity, $parameters['events']);
|
||||
}
|
||||
|
||||
// Remove events which weren't in the PUT request
|
||||
if (!$isNew && 'PUT' === $method) {
|
||||
foreach ($currentEvents as $currentEvent) {
|
||||
if (!in_array($currentEvent->getId(), $requestTriggerIds)) {
|
||||
$entity->removeTriggerEvent($currentEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FormInterface<mixed>
|
||||
*/
|
||||
protected function createTriggerEventEntityForm($entity): FormInterface
|
||||
{
|
||||
$triggerEventModel = $this->getModel('point.triggerevent');
|
||||
\assert($triggerEventModel instanceof TriggerEventModel);
|
||||
|
||||
return $triggerEventModel->createForm(
|
||||
$entity,
|
||||
$this->formFactory,
|
||||
null,
|
||||
[
|
||||
'csrf_protection' => false,
|
||||
'allow_extra_fields' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array of available point trigger event types.
|
||||
*/
|
||||
public function getPointTriggerEventTypesAction()
|
||||
{
|
||||
if (!$this->security->isGranted([$this->permissionBase.':view', $this->permissionBase.':viewown'])) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$eventTypesRaw = $this->model->getEvents();
|
||||
$eventTypes = [];
|
||||
|
||||
foreach ($eventTypesRaw as $key => $type) {
|
||||
$eventTypes[$key] = $type['label'];
|
||||
}
|
||||
|
||||
$view = $this->view(['eventTypes' => $eventTypes]);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete events from a point trigger.
|
||||
*
|
||||
* @param int $triggerId
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function deletePointTriggerEventsAction($triggerId)
|
||||
{
|
||||
if (!$this->security->isGranted([$this->permissionBase.':editown', $this->permissionBase.':editother'], 'MATCH_ONE')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$entity = $this->model->getEntity($triggerId);
|
||||
|
||||
if (null === $entity) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$eventsToDelete = $this->requestStack->getCurrentRequest()->get('events');
|
||||
$currentEvents = $entity->getEvents();
|
||||
|
||||
if (!is_array($eventsToDelete)) {
|
||||
return $this->badRequest('The events attribute must be array.');
|
||||
}
|
||||
|
||||
foreach ($currentEvents as $currentEvent) {
|
||||
if (in_array($currentEvent->getId(), $eventsToDelete)) {
|
||||
$entity->removeTriggerEvent($currentEvent);
|
||||
}
|
||||
}
|
||||
|
||||
$view = $this->view([$this->entityNameOne => $entity]);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\AbstractStandardFormController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class GroupController extends AbstractStandardFormController
|
||||
{
|
||||
protected function getTemplateBase(): string
|
||||
{
|
||||
return '@MauticPoint/Group';
|
||||
}
|
||||
|
||||
protected function getModelName(): string
|
||||
{
|
||||
return 'point.group';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $page
|
||||
*/
|
||||
public function indexAction(Request $request, $page = 1): Response
|
||||
{
|
||||
return parent::indexStandard($request, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates new form and processes post data.
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
public function newAction(Request $request)
|
||||
{
|
||||
return parent::newStandard($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates edit form and processes post data.
|
||||
*
|
||||
* @param int $objectId
|
||||
* @param bool $ignorePost
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
public function editAction(Request $request, $objectId, $ignorePost = false)
|
||||
{
|
||||
return parent::editStandard($request, $objectId, $ignorePost);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the entity.
|
||||
*
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return JsonResponse|RedirectResponse
|
||||
*/
|
||||
public function deleteAction(Request $request, $objectId)
|
||||
{
|
||||
return parent::deleteStandard($request, $objectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a group of entities.
|
||||
*
|
||||
* @return JsonResponse|RedirectResponse
|
||||
*/
|
||||
public function batchDeleteAction(Request $request)
|
||||
{
|
||||
return parent::batchDeleteStandard($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\AbstractFormController;
|
||||
use Mautic\CoreBundle\Factory\PageHelperFactoryInterface;
|
||||
use Mautic\PointBundle\Entity\Point;
|
||||
use Mautic\PointBundle\Model\PointModel;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class PointController extends AbstractFormController
|
||||
{
|
||||
/**
|
||||
* @param int $page
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
public function indexAction(Request $request, PageHelperFactoryInterface $pageHelperFactory, $page = 1)
|
||||
{
|
||||
// set some permissions
|
||||
$permissions = $this->security->isGranted([
|
||||
'point:points:view',
|
||||
'point:points:create',
|
||||
'point:points:edit',
|
||||
'point:points:delete',
|
||||
'point:points:publish',
|
||||
], 'RETURN_ARRAY');
|
||||
|
||||
if (!$permissions['point:points:view']) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$pageHelper = $pageHelperFactory->make('mautic.point', $page);
|
||||
|
||||
$limit = $pageHelper->getLimit();
|
||||
$start = $pageHelper->getStart();
|
||||
$search = $request->get('search', $request->getSession()->get('mautic.point.filter', ''));
|
||||
$filter = ['string' => $search, 'force' => []];
|
||||
$orderBy = $request->getSession()->get('mautic.point.orderby', 'p.name');
|
||||
$orderByDir = $request->getSession()->get('mautic.point.orderbydir', 'ASC');
|
||||
$pointModel = $this->getModel('point');
|
||||
\assert($pointModel instanceof PointModel);
|
||||
$points = $pointModel->getEntities([
|
||||
'start' => $start,
|
||||
'limit' => $limit,
|
||||
'filter' => $filter,
|
||||
'orderBy' => $orderBy,
|
||||
'orderByDir' => $orderByDir,
|
||||
]);
|
||||
|
||||
$request->getSession()->set('mautic.point.filter', $search);
|
||||
|
||||
$count = count($points);
|
||||
if ($count && $count < ($start + 1)) {
|
||||
$lastPage = $pageHelper->countPage($count);
|
||||
$returnUrl = $this->generateUrl('mautic_point_index', ['page' => $lastPage]);
|
||||
$pageHelper->rememberPage($lastPage);
|
||||
|
||||
return $this->postActionRedirect([
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $lastPage],
|
||||
'contentTemplate' => 'Mautic\PointBundle\Controller\PointController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_point_index',
|
||||
'mauticContent' => 'point',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$pageHelper->rememberPage($page);
|
||||
|
||||
// get the list of actions
|
||||
$actions = $pointModel->getPointActions();
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'searchValue' => $search,
|
||||
'items' => $points,
|
||||
'actions' => $actions['actions'],
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'permissions' => $permissions,
|
||||
'tmpl' => $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index',
|
||||
],
|
||||
'contentTemplate' => '@MauticPoint/Point/list.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_point_index',
|
||||
'mauticContent' => 'point',
|
||||
'route' => $this->generateUrl('mautic_point_index', ['page' => $page]),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates new form and processes post data.
|
||||
*
|
||||
* @param Point $entity
|
||||
*
|
||||
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function newAction(Request $request, FormFactoryInterface $formFactory, $entity = null)
|
||||
{
|
||||
$model = $this->getModel('point');
|
||||
\assert($model instanceof PointModel);
|
||||
|
||||
if (!($entity instanceof Point)) {
|
||||
/** @var Point $entity */
|
||||
$entity = $model->getEntity();
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted('point:points:create')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
// set the page we came from
|
||||
$page = $request->getSession()->get('mautic.point.page', 1);
|
||||
$method = $request->getMethod();
|
||||
$point = $request->request->all()['point'] ?? [];
|
||||
$actionType = 'POST' === $method ? ($point['type'] ?? '') : '';
|
||||
$action = $this->generateUrl('mautic_point_action', ['objectAction' => 'new']);
|
||||
$actions = $model->getPointActions();
|
||||
$form = $model->createForm($entity, $formFactory, $action, [
|
||||
'pointActions' => $actions,
|
||||
'actionType' => $actionType,
|
||||
]);
|
||||
$viewParameters = ['page' => $page];
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if (Request::METHOD_POST === $method) {
|
||||
$valid = false;
|
||||
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($entity);
|
||||
|
||||
$this->addFlashMessage('mautic.core.notice.created', [
|
||||
'%name%' => $entity->getName(),
|
||||
'%menu_link%' => 'mautic_point_index',
|
||||
'%url%' => $this->generateUrl('mautic_point_action', [
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $entity->getId(),
|
||||
]),
|
||||
]);
|
||||
|
||||
if ($this->getFormButton($form, ['buttons', 'save'])->isClicked()) {
|
||||
$returnUrl = $this->generateUrl('mautic_point_index', $viewParameters);
|
||||
$template = 'Mautic\PointBundle\Controller\PointController::indexAction';
|
||||
} else {
|
||||
// return edit view so that all the session stuff is loaded
|
||||
return $this->editAction($request, $formFactory, $entity->getId(), true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$returnUrl = $this->generateUrl('mautic_point_index', $viewParameters);
|
||||
$template = 'Mautic\PointBundle\Controller\PointController::indexAction';
|
||||
}
|
||||
|
||||
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
|
||||
return $this->postActionRedirect([
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => $viewParameters,
|
||||
'contentTemplate' => $template,
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_point_index',
|
||||
'mauticContent' => 'point',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$themes = ['@MauticPoint/FormTheme/Action/_pointaction_properties_row.html.twig'];
|
||||
if ($actionType && !empty($actions['actions'][$actionType]['formTheme'])) {
|
||||
$themes[] = $actions['actions'][$actionType]['formTheme'];
|
||||
}
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'tmpl' => $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index',
|
||||
'entity' => $entity,
|
||||
'form' => $form->createView(),
|
||||
'actions' => $actions['actions'],
|
||||
'formThemes' => $themes,
|
||||
],
|
||||
'contentTemplate' => '@MauticPoint/Point/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_point_index',
|
||||
'mauticContent' => 'point',
|
||||
'route' => $this->generateUrl('mautic_point_action', [
|
||||
'objectAction' => (!empty($valid) ? 'edit' : 'new'), // valid means a new form was applied
|
||||
'objectId' => $entity->getId(),
|
||||
]
|
||||
),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates edit form and processes post data.
|
||||
*
|
||||
* @param int $objectId
|
||||
* @param bool $ignorePost
|
||||
*
|
||||
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function editAction(Request $request, FormFactoryInterface $formFactory, $objectId, $ignorePost = false)
|
||||
{
|
||||
$model = $this->getModel('point');
|
||||
\assert($model instanceof PointModel);
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
// set the page we came from
|
||||
$page = $request->getSession()->get('mautic.point.page', 1);
|
||||
|
||||
$viewParameters = ['page' => $page];
|
||||
|
||||
// set the return URL
|
||||
$returnUrl = $this->generateUrl('mautic_point_index', ['page' => $page]);
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => $viewParameters,
|
||||
'contentTemplate' => 'Mautic\PointBundle\Controller\PointController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_point_index',
|
||||
'mauticContent' => 'point',
|
||||
],
|
||||
];
|
||||
|
||||
// form not found
|
||||
if (null === $entity) {
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => [
|
||||
[
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.point.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
],
|
||||
],
|
||||
])
|
||||
);
|
||||
} elseif (!$this->security->isGranted('point:points:edit')) {
|
||||
return $this->accessDenied();
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
// deny access if the entity is locked
|
||||
return $this->isLocked($postActionVars, $entity, 'point');
|
||||
}
|
||||
|
||||
$method = $request->getMethod();
|
||||
$point = $request->request->all()['point'] ?? [];
|
||||
$actionType = 'POST' === $method ? ($point['type'] ?? '') : $entity->getType();
|
||||
|
||||
$action = $this->generateUrl('mautic_point_action', ['objectAction' => 'edit', 'objectId' => $objectId]);
|
||||
$actions = $model->getPointActions();
|
||||
$form = $model->createForm($entity, $formFactory, $action, [
|
||||
'pointActions' => $actions,
|
||||
'actionType' => $actionType,
|
||||
]);
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if (!$ignorePost && 'POST' === $method) {
|
||||
$valid = false;
|
||||
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($entity, $this->getFormButton($form, ['buttons', 'save'])->isClicked());
|
||||
|
||||
$this->addFlashMessage('mautic.core.notice.updated', [
|
||||
'%name%' => $entity->getName(),
|
||||
'%menu_link%' => 'mautic_point_index',
|
||||
'%url%' => $this->generateUrl('mautic_point_action', [
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $entity->getId(),
|
||||
]),
|
||||
]);
|
||||
|
||||
if ($this->getFormButton($form, ['buttons', 'save'])->isClicked()) {
|
||||
$returnUrl = $this->generateUrl('mautic_point_index', $viewParameters);
|
||||
$template = 'Mautic\PointBundle\Controller\PointController::indexAction';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// unlock the entity
|
||||
$model->unlockEntity($entity);
|
||||
|
||||
$returnUrl = $this->generateUrl('mautic_point_index', $viewParameters);
|
||||
$template = 'Mautic\PointBundle\Controller\PointController::indexAction';
|
||||
}
|
||||
|
||||
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => $viewParameters,
|
||||
'contentTemplate' => $template,
|
||||
])
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// lock the entity
|
||||
$model->lockEntity($entity);
|
||||
}
|
||||
|
||||
$themes = ['@MauticPoint/FormTheme/Action/_pointaction_properties_row.html.twig'];
|
||||
if (!empty($actions['actions'][$actionType]['formTheme'])) {
|
||||
$themes[] = $actions['actions'][$actionType]['formTheme'];
|
||||
}
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'tmpl' => $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index',
|
||||
'entity' => $entity,
|
||||
'form' => $form->createView(),
|
||||
'actions' => $actions['actions'],
|
||||
'formThemes' => $themes,
|
||||
],
|
||||
'contentTemplate' => '@MauticPoint/Point/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_point_index',
|
||||
'mauticContent' => 'point',
|
||||
'route' => $this->generateUrl('mautic_point_action', [
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $entity->getId(),
|
||||
]
|
||||
),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone an entity.
|
||||
*
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return array|JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function cloneAction(Request $request, FormFactoryInterface $formFactory, $objectId)
|
||||
{
|
||||
$model = $this->getModel('point');
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if (null != $entity) {
|
||||
if (!$this->security->isGranted('point:points:create')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$entity = clone $entity;
|
||||
$entity->setIsPublished(false);
|
||||
}
|
||||
|
||||
return $this->newAction($request, $formFactory, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the entity.
|
||||
*
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function deleteAction(Request $request, $objectId)
|
||||
{
|
||||
$page = $request->getSession()->get('mautic.point.page', 1);
|
||||
$returnUrl = $this->generateUrl('mautic_point_index', ['page' => $page]);
|
||||
$flashes = [];
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\PointBundle\Controller\PointController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_point_index',
|
||||
'mauticContent' => 'point',
|
||||
],
|
||||
];
|
||||
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$model = $this->getModel('point');
|
||||
\assert($model instanceof PointModel);
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if (null === $entity) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.point.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
} elseif (!$this->security->isGranted('point:points:delete')) {
|
||||
return $this->accessDenied();
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
return $this->isLocked($postActionVars, $entity, 'point');
|
||||
}
|
||||
|
||||
$model->deleteEntity($entity);
|
||||
|
||||
$identifier = $this->translator->trans($entity->getName());
|
||||
$flashes[] = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.core.notice.deleted',
|
||||
'msgVars' => [
|
||||
'%name%' => $identifier,
|
||||
'%id%' => $objectId,
|
||||
],
|
||||
];
|
||||
} // else don't do anything
|
||||
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => $flashes,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a group of entities.
|
||||
*/
|
||||
public function batchDeleteAction(Request $request): Response
|
||||
{
|
||||
$page = $request->getSession()->get('mautic.point.page', 1);
|
||||
$returnUrl = $this->generateUrl('mautic_point_index', ['page' => $page]);
|
||||
$flashes = [];
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\PointBundle\Controller\PointController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_point_index',
|
||||
'mauticContent' => 'point',
|
||||
],
|
||||
];
|
||||
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$model = $this->getModel('point');
|
||||
\assert($model instanceof PointModel);
|
||||
$ids = json_decode($request->query->get('ids', '{}'));
|
||||
$deleteIds = [];
|
||||
|
||||
// Loop over the IDs to perform access checks pre-delete
|
||||
foreach ($ids as $objectId) {
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if (null === $entity) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.point.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
} elseif (!$this->security->isGranted('point:points:delete')) {
|
||||
$flashes[] = $this->accessDenied(true);
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
$flashes[] = $this->isLocked($postActionVars, $entity, 'point', true);
|
||||
} else {
|
||||
$deleteIds[] = $objectId;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete everything we are able to
|
||||
if (!empty($deleteIds)) {
|
||||
$entities = $model->deleteEntities($deleteIds);
|
||||
|
||||
$flashes[] = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.point.notice.batch_deleted',
|
||||
'msgVars' => [
|
||||
'%count%' => count($entities),
|
||||
],
|
||||
];
|
||||
}
|
||||
} // else don't do anything
|
||||
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => $flashes,
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,626 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\FormController;
|
||||
use Mautic\CoreBundle\Factory\PageHelperFactoryInterface;
|
||||
use Mautic\PointBundle\Entity\Trigger;
|
||||
use Mautic\PointBundle\Model\TriggerEventModel;
|
||||
use Mautic\PointBundle\Model\TriggerModel;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class TriggerController extends FormController
|
||||
{
|
||||
/**
|
||||
* @param int $page
|
||||
*
|
||||
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function indexAction(Request $request, PageHelperFactoryInterface $pageHelperFactory, $page = 1)
|
||||
{
|
||||
// set some permissions
|
||||
$permissions = $this->security->isGranted([
|
||||
'point:triggers:view',
|
||||
'point:triggers:create',
|
||||
'point:triggers:edit',
|
||||
'point:triggers:delete',
|
||||
'point:triggers:publish',
|
||||
], 'RETURN_ARRAY');
|
||||
|
||||
if (!$permissions['point:triggers:view']) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$pageHelper = $pageHelperFactory->make('mautic.point.trigger', $page);
|
||||
|
||||
$limit = $pageHelper->getLimit();
|
||||
$start = $pageHelper->getStart();
|
||||
$search = $request->get('search', $request->getSession()->get('mautic.point.trigger.filter', ''));
|
||||
$filter = ['string' => $search, 'force' => []];
|
||||
$orderBy = $request->getSession()->get('mautic.point.trigger.orderby', 't.name');
|
||||
$orderByDir = $request->getSession()->get('mautic.point.trigger.orderbydir', 'ASC');
|
||||
$triggers = $this->getModel('point.trigger')->getEntities(
|
||||
[
|
||||
'start' => $start,
|
||||
'limit' => $limit,
|
||||
'filter' => $filter,
|
||||
'orderBy' => $orderBy,
|
||||
'orderByDir' => $orderByDir,
|
||||
]
|
||||
);
|
||||
|
||||
$request->getSession()->set('mautic.point.trigger.filter', $search);
|
||||
|
||||
$count = count($triggers);
|
||||
if ($count && $count < ($start + 1)) {
|
||||
$lastPage = $pageHelper->countPage($count);
|
||||
$returnUrl = $this->generateUrl('mautic_pointtrigger_index', ['page' => $lastPage]);
|
||||
$pageHelper->rememberPage($lastPage);
|
||||
|
||||
return $this->postActionRedirect([
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $lastPage],
|
||||
'contentTemplate' => 'Mautic\PointBundle\Controller\TriggerController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_pointtrigger_index',
|
||||
'mauticContent' => 'pointTrigger',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$pageHelper->rememberPage($page);
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'searchValue' => $search,
|
||||
'items' => $triggers,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'permissions' => $permissions,
|
||||
'tmpl' => $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index',
|
||||
],
|
||||
'contentTemplate' => '@MauticPoint/Trigger/list.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_pointtrigger_index',
|
||||
'mauticContent' => 'pointTrigger',
|
||||
'route' => $this->generateUrl('mautic_pointtrigger_index', ['page' => $page]),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* View a specific trigger.
|
||||
*
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return array|JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function viewAction(Request $request, $objectId)
|
||||
{
|
||||
$entity = $this->getModel('point.trigger')->getEntity($objectId);
|
||||
|
||||
// set the page we came from
|
||||
$page = $request->getSession()->get('mautic.point.trigger.page', 1);
|
||||
|
||||
$permissions = $this->security->isGranted([
|
||||
'point:triggers:view',
|
||||
'point:triggers:create',
|
||||
'point:triggers:edit',
|
||||
'point:triggers:delete',
|
||||
'point:triggers:publish',
|
||||
], 'RETURN_ARRAY');
|
||||
|
||||
if (null === $entity) {
|
||||
// set the return URL
|
||||
$returnUrl = $this->generateUrl('mautic_pointtrigger_index', ['page' => $page]);
|
||||
|
||||
return $this->postActionRedirect([
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\PointBundle\Controller\TriggerController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_pointtrigger_index',
|
||||
'mauticContent' => 'pointTrigger',
|
||||
],
|
||||
'flashes' => [
|
||||
[
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.point.trigger.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
],
|
||||
],
|
||||
]);
|
||||
} elseif (!$permissions['point:triggers:view']) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'entity' => $entity,
|
||||
'page' => $page,
|
||||
'permissions' => $permissions,
|
||||
],
|
||||
'contentTemplate' => '@MauticPoint/Trigger/details.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_pointtrigger_index',
|
||||
'mauticContent' => 'pointTrigger',
|
||||
'route' => $this->generateUrl('mautic_pointtrigger_action', [
|
||||
'objectAction' => 'view',
|
||||
'objectId' => $entity->getId(), ]
|
||||
),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates new form and processes post data.
|
||||
*
|
||||
* @param array<mixed> $triggerEvents
|
||||
*/
|
||||
public function newAction(Request $request, ?Trigger $entity = null, array $triggerEvents = []): Response
|
||||
{
|
||||
/** @var TriggerModel $model */
|
||||
$model = $this->getModel('point.trigger');
|
||||
|
||||
if (!($entity instanceof Trigger)) {
|
||||
/** @var Trigger $entity */
|
||||
$entity = $model->getEntity();
|
||||
}
|
||||
|
||||
$session = $request->getSession();
|
||||
$sessionId = $this->getSessionBase();
|
||||
|
||||
if (!$this->security->isGranted('point:triggers:create')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
// set the page we came from
|
||||
$page = $request->getSession()->get('mautic.point.trigger.page', 1);
|
||||
|
||||
// set added/updated events
|
||||
$addEvents = $session->get('mautic.point.'.$sessionId.'.triggerevents.modified', []);
|
||||
if (!empty($triggerEvents)) {
|
||||
$addEvents += $triggerEvents;
|
||||
$session->set('mautic.point.'.$sessionId.'.triggerevents.modified', $triggerEvents);
|
||||
}
|
||||
$deletedEvents = $session->get('mautic.point.'.$sessionId.'.triggerevents.deleted', []);
|
||||
|
||||
$action = $this->generateUrl('mautic_pointtrigger_action', ['objectAction' => 'new']);
|
||||
$form = $model->createForm($entity, $this->formFactory, $action);
|
||||
$form->get('sessionId')->setData($sessionId);
|
||||
|
||||
// Check for a submitted form and process it
|
||||
if ('POST' == $request->getMethod()) {
|
||||
$valid = false;
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
// only save events that are not to be deleted
|
||||
$events = array_diff_key($addEvents, array_flip($deletedEvents));
|
||||
|
||||
// make sure that at least one action is selected
|
||||
if (empty($events)) {
|
||||
// set the error
|
||||
$form->addError(new FormError(
|
||||
$this->translator->trans('mautic.core.value.required', [], 'validators')
|
||||
));
|
||||
$valid = false;
|
||||
} else {
|
||||
$model->setEvents($entity, $events);
|
||||
|
||||
$model->saveEntity($entity);
|
||||
|
||||
$this->addFlashMessage('mautic.core.notice.created', [
|
||||
'%name%' => $entity->getName(),
|
||||
'%menu_link%' => 'mautic_pointtrigger_index',
|
||||
'%url%' => $this->generateUrl('mautic_pointtrigger_action', [
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $entity->getId(),
|
||||
]),
|
||||
]);
|
||||
|
||||
if (!$this->getFormButton($form, ['buttons', 'save'])->isClicked()) {
|
||||
// unset the clone session.
|
||||
$this->clearSessionComponents($request, $sessionId);
|
||||
|
||||
// return edit view so that all the session stuff is loaded
|
||||
return $this->editAction($request, $entity->getId(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
|
||||
$viewParameters = ['page' => $page];
|
||||
$returnUrl = $this->generateUrl('mautic_pointtrigger_index', $viewParameters);
|
||||
$template = 'Mautic\PointBundle\Controller\TriggerController::indexAction';
|
||||
|
||||
// clear temporary fields
|
||||
$this->clearSessionComponents($request, $sessionId);
|
||||
|
||||
return $this->postActionRedirect([
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => $viewParameters,
|
||||
'contentTemplate' => $template,
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_pointtrigger_index',
|
||||
'mauticContent' => 'pointTrigger',
|
||||
],
|
||||
]);
|
||||
}
|
||||
} elseif (!empty($triggerEvents)) {
|
||||
// The clone part, no need to clear session here.
|
||||
$addEvents = $triggerEvents;
|
||||
$deletedEvents = [];
|
||||
} else {
|
||||
// clear out existing fields in case the form was refreshed, browser closed, etc
|
||||
$this->clearSessionComponents($request, $sessionId);
|
||||
}
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'events' => $model->getEventGroups(),
|
||||
'triggerEvents' => $addEvents,
|
||||
'deletedEvents' => $deletedEvents,
|
||||
'tmpl' => $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index',
|
||||
'entity' => $entity,
|
||||
'form' => $form->createView(),
|
||||
'sessionId' => $sessionId,
|
||||
],
|
||||
'contentTemplate' => '@MauticPoint/Trigger/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_pointtrigger_index',
|
||||
'mauticContent' => 'pointTrigger',
|
||||
'route' => $this->generateUrl('mautic_pointtrigger_action', [
|
||||
'objectAction' => (!empty($valid) ? 'edit' : 'new'), // valid means a new form was applied
|
||||
'objectId' => $entity->getId(), ]
|
||||
),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates edit form and processes post data.
|
||||
*
|
||||
* @param int $objectId
|
||||
* @param bool $ignorePost
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
public function editAction(Request $request, $objectId, $ignorePost = false)
|
||||
{
|
||||
/** @var TriggerModel $model */
|
||||
$model = $this->getModel('point.trigger');
|
||||
$entity = $model->getEntity($objectId);
|
||||
$session = $request->getSession();
|
||||
$cleanSlate = true;
|
||||
|
||||
// set the page we came from
|
||||
$page = $request->getSession()->get('mautic.point.trigger.page', 1);
|
||||
|
||||
// set the return URL
|
||||
$returnUrl = $this->generateUrl('mautic_pointtrigger_index', ['page' => $page]);
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\PointBundle\Controller\TriggerController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_pointtrigger_index',
|
||||
'mauticContent' => 'pointTrigger',
|
||||
],
|
||||
];
|
||||
|
||||
// form not found
|
||||
if (null === $entity) {
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => [
|
||||
[
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.point.trigger.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
],
|
||||
],
|
||||
])
|
||||
);
|
||||
} elseif (!$this->security->isGranted('point:triggers:edit')) {
|
||||
return $this->accessDenied();
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
// deny access if the entity is locked
|
||||
return $this->isLocked($postActionVars, $entity, 'point.trigger');
|
||||
}
|
||||
|
||||
$action = $this->generateUrl('mautic_pointtrigger_action', ['objectAction' => 'edit', 'objectId' => $objectId]);
|
||||
$form = $model->createForm($entity, $this->formFactory, $action);
|
||||
$form->get('sessionId')->setData($objectId);
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if (!$ignorePost && 'POST' == $request->getMethod()) {
|
||||
$valid = false;
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
// set added/updated events
|
||||
$addEvents = $session->get('mautic.point.'.$objectId.'.triggerevents.modified', []);
|
||||
$deletedEvents = $session->get('mautic.point.'.$objectId.'.triggerevents.deleted', []);
|
||||
$events = array_diff_key($addEvents, array_flip($deletedEvents));
|
||||
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
// make sure that at least one field is selected
|
||||
if (empty($events)) {
|
||||
// set the error
|
||||
$form->addError(new FormError(
|
||||
$this->translator->trans('mautic.core.value.required', [], 'validators')
|
||||
));
|
||||
$valid = false;
|
||||
} else {
|
||||
$model->setEvents($entity, $events);
|
||||
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($entity, $this->getFormButton($form, ['buttons', 'save'])->isClicked());
|
||||
|
||||
// delete entities
|
||||
if (count($deletedEvents)) {
|
||||
$triggerEventModel = $this->getModel('point.triggerevent');
|
||||
\assert($triggerEventModel instanceof TriggerEventModel);
|
||||
$triggerEventModel->deleteEntities($deletedEvents);
|
||||
}
|
||||
|
||||
$session->set('mautic.point.'.$objectId.'.triggerevents.modified', $events);
|
||||
$session->set('mautic.point.'.$objectId.'.triggerevents.deleted', []);
|
||||
|
||||
$this->addFlashMessage('mautic.core.notice.updated', [
|
||||
'%name%' => $entity->getName(),
|
||||
'%menu_link%' => 'mautic_pointtrigger_index',
|
||||
'%url%' => $this->generateUrl('mautic_pointtrigger_action', [
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $entity->getId(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// unlock the entity
|
||||
$model->unlockEntity($entity);
|
||||
}
|
||||
|
||||
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
|
||||
$viewParameters = ['page' => $page];
|
||||
$returnUrl = $this->generateUrl('mautic_pointtrigger_index', $viewParameters);
|
||||
$template = 'Mautic\PointBundle\Controller\TriggerController::indexAction';
|
||||
|
||||
// remove fields from session
|
||||
$this->clearSessionComponents($request, $objectId);
|
||||
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => $viewParameters,
|
||||
'contentTemplate' => $template,
|
||||
])
|
||||
);
|
||||
} elseif ($form->get('buttons')->get('apply')->isClicked()) {
|
||||
// do not clear session, just reload view with updated session
|
||||
$cleanSlate = false;
|
||||
}
|
||||
} else {
|
||||
$cleanSlate = true;
|
||||
|
||||
// lock the entity
|
||||
$model->lockEntity($entity);
|
||||
}
|
||||
|
||||
$triggerEvents = [];
|
||||
|
||||
if ($cleanSlate) {
|
||||
// clean slate
|
||||
$this->clearSessionComponents($request, $objectId);
|
||||
|
||||
// load existing events into session
|
||||
$existingActions = $entity->getEvents()->toArray();
|
||||
foreach ($existingActions as $a) {
|
||||
$id = $a->getId();
|
||||
$action = $a->convertToArray();
|
||||
unset($action['form']);
|
||||
$triggerEvents[$id] = $action;
|
||||
}
|
||||
$session->set('mautic.point.'.$objectId.'.triggerevents.modified', $triggerEvents);
|
||||
$deletedEvents = [];
|
||||
}
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'events' => $model->getEventGroups(),
|
||||
'triggerEvents' => $triggerEvents,
|
||||
'deletedEvents' => $deletedEvents,
|
||||
'tmpl' => $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index',
|
||||
'entity' => $entity,
|
||||
'form' => $form->createView(),
|
||||
'sessionId' => $objectId,
|
||||
],
|
||||
'contentTemplate' => '@MauticPoint/Trigger/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_pointtrigger_index',
|
||||
'mauticContent' => 'pointTrigger',
|
||||
'route' => $this->generateUrl('mautic_pointtrigger_action', [
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $entity->getId(), ]
|
||||
),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone an entity.
|
||||
*
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return array|JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function cloneAction(Request $request, $objectId)
|
||||
{
|
||||
/** @var TriggerModel $model */
|
||||
$model = $this->getModel('point.trigger');
|
||||
/** @var Trigger $entity */
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
$triggerEvents = [];
|
||||
|
||||
if (null != $entity) {
|
||||
if (!$this->security->isGranted('point:triggers:create')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$existingActions = $entity->getEvents()->toArray();
|
||||
|
||||
$entity = clone $entity;
|
||||
$entity->setIsPublished(false);
|
||||
foreach ($existingActions as $key => $action) {
|
||||
$action = clone $action;
|
||||
$action->setTrigger($entity);
|
||||
$entity->addTriggerEvent($key, $action);
|
||||
$actionArray = $action->convertToArray();
|
||||
unset($actionArray['form']);
|
||||
$triggerEvents[] = $actionArray;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->newAction($request, $entity, $triggerEvents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the entity.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function deleteAction(Request $request, $objectId)
|
||||
{
|
||||
$page = $request->getSession()->get('mautic.point.trigger.page', 1);
|
||||
$returnUrl = $this->generateUrl('mautic_pointtrigger_index', ['page' => $page]);
|
||||
$flashes = [];
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\PointBundle\Controller\TriggerController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_pointtrigger_index',
|
||||
'mauticContent' => 'pointTrigger',
|
||||
],
|
||||
];
|
||||
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$model = $this->getModel('point.trigger');
|
||||
\assert($model instanceof TriggerModel);
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if (null === $entity) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.point.trigger.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
} elseif (!$this->security->isGranted('point:triggers:delete')) {
|
||||
return $this->accessDenied();
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
return $this->isLocked($postActionVars, $entity, 'point.trigger');
|
||||
}
|
||||
|
||||
$model->deleteEntity($entity);
|
||||
|
||||
$identifier = $this->translator->trans($entity->getName());
|
||||
$flashes[] = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.core.notice.deleted',
|
||||
'msgVars' => [
|
||||
'%name%' => $identifier,
|
||||
'%id%' => $objectId,
|
||||
],
|
||||
];
|
||||
} // else don't do anything
|
||||
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => $flashes,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a group of entities.
|
||||
*/
|
||||
public function batchDeleteAction(Request $request): Response
|
||||
{
|
||||
$page = $request->getSession()->get('mautic.point.trigger.page', 1);
|
||||
$returnUrl = $this->generateUrl('mautic_pointtrigger_index', ['page' => $page]);
|
||||
$flashes = [];
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\PointBundle\Controller\TriggerController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_pointtrigger_index',
|
||||
'mauticContent' => 'pointTrigger',
|
||||
],
|
||||
];
|
||||
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$model = $this->getModel('point.trigger');
|
||||
\assert($model instanceof TriggerModel);
|
||||
$ids = json_decode($request->query->get('ids', '{}'));
|
||||
$deleteIds = [];
|
||||
|
||||
// Loop over the IDs to perform access checks pre-delete
|
||||
foreach ($ids as $objectId) {
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if (null === $entity) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.point.trigger.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
} elseif (!$this->security->isGranted('point:triggers:delete')) {
|
||||
$flashes[] = $this->accessDenied(true);
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
$flashes[] = $this->isLocked($postActionVars, $entity, 'point.trigger', true);
|
||||
} else {
|
||||
$deleteIds[] = $objectId;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete everything we are able to
|
||||
if (!empty($deleteIds)) {
|
||||
$entities = $model->deleteEntities($deleteIds);
|
||||
|
||||
$flashes[] = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.point.trigger.notice.batch_deleted',
|
||||
'msgVars' => [
|
||||
'%count%' => count($entities),
|
||||
],
|
||||
];
|
||||
}
|
||||
} // else don't do anything
|
||||
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => $flashes,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear field and events from the session.
|
||||
*/
|
||||
private function clearSessionComponents(Request $request, $sessionId): void
|
||||
{
|
||||
$session = $request->getSession();
|
||||
$session->remove('mautic.point.'.$sessionId.'.triggerevents.modified');
|
||||
$session->remove('mautic.point.'.$sessionId.'.triggerevents.deleted');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\FormController as CommonFormController;
|
||||
use Mautic\PointBundle\Entity\TriggerEvent;
|
||||
use Mautic\PointBundle\Form\Type\TriggerEventType;
|
||||
use Mautic\PointBundle\Model\TriggerModel;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class TriggerEventController extends CommonFormController
|
||||
{
|
||||
/**
|
||||
* Generates new form and processes post data.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function newAction(Request $request)
|
||||
{
|
||||
$success = 0;
|
||||
$valid = $cancelled = false;
|
||||
$method = $request->getMethod();
|
||||
$session = $request->getSession();
|
||||
|
||||
if ('POST' == $method) {
|
||||
$triggerEvent = $request->request->all()['pointtriggerevent'] ?? [];
|
||||
$eventType = $triggerEvent['type'];
|
||||
$triggerId = $triggerEvent['triggerId'];
|
||||
} else {
|
||||
$eventType = $request->query->get('type');
|
||||
$triggerId = $request->query->get('triggerId');
|
||||
|
||||
$triggerEvent = [
|
||||
'type' => $eventType,
|
||||
'triggerId' => $triggerId,
|
||||
];
|
||||
}
|
||||
|
||||
// ajax only for form fields
|
||||
if (!$eventType
|
||||
|| !$request->isXmlHttpRequest()
|
||||
|| !$this->security->isGranted([
|
||||
'point:triggers:edit',
|
||||
'point:triggers:create',
|
||||
], 'MATCH_ONE')
|
||||
) {
|
||||
return $this->modalAccessDenied();
|
||||
}
|
||||
|
||||
// fire the builder event
|
||||
/** @var TriggerModel $pointTriggerModel */
|
||||
$pointTriggerModel = $this->getModel('point.trigger');
|
||||
\assert($pointTriggerModel instanceof TriggerModel);
|
||||
$events = $pointTriggerModel->getEvents();
|
||||
$form = $this->formFactory->create(TriggerEventType::class, $triggerEvent, [
|
||||
'action' => $this->generateUrl('mautic_pointtriggerevent_action', ['objectAction' => 'new']),
|
||||
'settings' => $events[$eventType],
|
||||
]);
|
||||
$form->get('triggerId')->setData($triggerId);
|
||||
$triggerEvent['settings'] = $events[$eventType];
|
||||
|
||||
// Check for a submitted form and process it
|
||||
if ('POST' == $method) {
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
$success = 1;
|
||||
|
||||
// form is valid so process the data
|
||||
$keyId = 'new'.hash('sha1', uniqid(mt_rand()));
|
||||
|
||||
// save the properties to session
|
||||
$actions = $session->get('mautic.point.'.$triggerId.'.triggerevents.modified');
|
||||
$formData = $form->getData();
|
||||
$triggerEvent = array_merge($triggerEvent, $formData);
|
||||
$triggerEvent['id'] = $keyId;
|
||||
if (empty($triggerEvent['name'])) {
|
||||
// set it to the event default
|
||||
$triggerEvent['name'] = $this->translator->trans($triggerEvent['settings']['label']);
|
||||
}
|
||||
$actions[$keyId] = $triggerEvent;
|
||||
$session->set('mautic.point.'.$triggerId.'.triggerevents.modified', $actions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$viewParams = ['type' => $eventType];
|
||||
if ($cancelled || $valid) {
|
||||
$closeModal = true;
|
||||
} else {
|
||||
if (isset($triggerEvent['settings']['formTheme'])) {
|
||||
$viewParams['formTheme'] = $triggerEvent['settings']['formTheme'];
|
||||
}
|
||||
|
||||
$closeModal = false;
|
||||
$viewParams['form'] = $form->createView();
|
||||
$header = $triggerEvent['settings']['label'];
|
||||
$viewParams['eventHeader'] = $this->translator->trans($header);
|
||||
}
|
||||
|
||||
$passthroughVars = [
|
||||
'mauticContent' => 'pointTriggerEvent',
|
||||
'success' => $success,
|
||||
'route' => false,
|
||||
];
|
||||
|
||||
if (!empty($keyId)) {
|
||||
// prevent undefined errors
|
||||
$entity = new TriggerEvent();
|
||||
$blank = $entity->convertToArray();
|
||||
$triggerEvent = array_merge($blank, $triggerEvent);
|
||||
|
||||
$template = (empty($triggerEvent['settings']['template'])) ? '@MauticPoint/Event/generic.html.twig'
|
||||
: $triggerEvent['settings']['template'];
|
||||
|
||||
$passthroughVars['eventId'] = $keyId;
|
||||
$passthroughVars['eventHtml'] = $this->renderView($template, [
|
||||
'event' => $triggerEvent,
|
||||
'id' => $keyId,
|
||||
'sessionId' => $triggerId,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($closeModal) {
|
||||
// just close the modal
|
||||
$passthroughVars['closeModal'] = 1;
|
||||
|
||||
return new JsonResponse($passthroughVars);
|
||||
}
|
||||
|
||||
return $this->ajaxAction($request, [
|
||||
'contentTemplate' => '@MauticPoint/Event/form.html.twig',
|
||||
'viewParameters' => $viewParams,
|
||||
'passthroughVars' => $passthroughVars,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates edit form and processes post data.
|
||||
*
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function editAction(Request $request, $objectId)
|
||||
{
|
||||
$session = $request->getSession();
|
||||
$method = $request->getMethod();
|
||||
$triggerEvent = $request->request->all()['pointtriggerevent'] ?? [];
|
||||
$triggerId = 'POST' === $method ? ($triggerEvent['triggerId'] ?? '') : $request->query->get('triggerId');
|
||||
$events = $session->get('mautic.point.'.$triggerId.'.triggerevents.modified', []);
|
||||
$success = 0;
|
||||
$valid = $cancelled = false;
|
||||
$triggerEvent = array_key_exists($objectId, $events) ? $events[$objectId] : null;
|
||||
|
||||
if (null !== $triggerEvent) {
|
||||
$eventType = $triggerEvent['type'];
|
||||
$pointTriggerModel = $this->getModel('point.trigger');
|
||||
\assert($pointTriggerModel instanceof TriggerModel);
|
||||
$events = $pointTriggerModel->getEvents();
|
||||
$triggerEvent['settings'] = $events[$eventType];
|
||||
|
||||
// ajax only for form fields
|
||||
if (!$eventType
|
||||
|| !$request->isXmlHttpRequest()
|
||||
|| !$this->security->isGranted([
|
||||
'point:triggers:edit',
|
||||
'point:triggers:create',
|
||||
], 'MATCH_ONE')
|
||||
) {
|
||||
return $this->modalAccessDenied();
|
||||
}
|
||||
|
||||
$form = $this->formFactory->create(TriggerEventType::class, $triggerEvent, [
|
||||
'action' => $this->generateUrl('mautic_pointtriggerevent_action', ['objectAction' => 'edit', 'objectId' => $objectId]),
|
||||
'settings' => $triggerEvent['settings'],
|
||||
]);
|
||||
$form->get('triggerId')->setData($triggerId);
|
||||
// Check for a submitted form and process it
|
||||
if ('POST' == $method) {
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
$success = 1;
|
||||
|
||||
// form is valid so process the data
|
||||
|
||||
// save the properties to session
|
||||
$session = $request->getSession();
|
||||
$events = $session->get('mautic.point.'.$triggerId.'.triggerevents.modified');
|
||||
/** @var array<mixed> $formData */
|
||||
$formData = $form->getData();
|
||||
// overwrite with updated data
|
||||
$triggerEvent = array_merge($events[$objectId], $formData);
|
||||
if (empty($triggerEvent['name'])) {
|
||||
// set it to the event default
|
||||
$triggerEvent['name'] = $this->translator->trans($triggerEvent['settings']['label']);
|
||||
}
|
||||
$events[$objectId] = $triggerEvent;
|
||||
$session->set('mautic.point.'.$triggerId.'.triggerevents.modified', $events);
|
||||
|
||||
// generate HTML for the field
|
||||
$keyId = $objectId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$viewParams = ['type' => $eventType];
|
||||
if ($cancelled || $valid) {
|
||||
$closeModal = true;
|
||||
} else {
|
||||
if (isset($triggerEvent['settings']['formTheme'])) {
|
||||
$viewParams['formTheme'] = $triggerEvent['settings']['formTheme'];
|
||||
}
|
||||
|
||||
$closeModal = false;
|
||||
$viewParams['form'] = $form->createView();
|
||||
$viewParams['eventHeader'] = $this->translator->trans($triggerEvent['settings']['label']);
|
||||
}
|
||||
|
||||
$passthroughVars = [
|
||||
'mauticContent' => 'pointTriggerEvent',
|
||||
'success' => $success,
|
||||
'route' => false,
|
||||
];
|
||||
|
||||
if (!empty($keyId)) {
|
||||
$passthroughVars['eventId'] = $keyId;
|
||||
|
||||
// prevent undefined errors
|
||||
$entity = new TriggerEvent();
|
||||
$blank = $entity->convertToArray();
|
||||
$triggerEvent = array_merge($blank, $triggerEvent);
|
||||
$template = (empty($triggerEvent['settings']['template'])) ? '@MauticPoint/Event/generic.html.twig'
|
||||
: $triggerEvent['settings']['template'];
|
||||
|
||||
$passthroughVars['eventId'] = $keyId;
|
||||
$passthroughVars['eventHtml'] = $this->renderView($template, [
|
||||
'event' => $triggerEvent,
|
||||
'id' => $keyId,
|
||||
'sessionId' => $triggerId,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($closeModal) {
|
||||
// just close the modal
|
||||
$passthroughVars['closeModal'] = 1;
|
||||
|
||||
return new JsonResponse($passthroughVars);
|
||||
}
|
||||
|
||||
return $this->ajaxAction($request, [
|
||||
'contentTemplate' => '@MauticPoint/Event/form.html.twig',
|
||||
'viewParameters' => $viewParams,
|
||||
'passthroughVars' => $passthroughVars,
|
||||
]);
|
||||
}
|
||||
|
||||
return new JsonResponse(['success' => 0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the entity.
|
||||
*
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function deleteAction(Request $request, $objectId)
|
||||
{
|
||||
$session = $request->getSession();
|
||||
$triggerId = $request->get('triggerId');
|
||||
$events = $session->get('mautic.point.'.$triggerId.'.triggerevents.modified', []);
|
||||
$delete = $session->get('mautic.point.'.$triggerId.'.triggerevents.deleted', []);
|
||||
|
||||
// ajax only for form fields
|
||||
if (!$request->isXmlHttpRequest()
|
||||
|| !$this->security->isGranted([
|
||||
'point:triggers:edit',
|
||||
'point:triggers:create',
|
||||
], 'MATCH_ONE')
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$triggerEvent = (array_key_exists($objectId, $events)) ? $events[$objectId] : null;
|
||||
|
||||
if ('POST' == $request->getMethod() && null !== $triggerEvent) {
|
||||
// add the field to the delete list
|
||||
if (!in_array($objectId, $delete)) {
|
||||
$delete[] = $objectId;
|
||||
$session->set('mautic.point.'.$triggerId.'.triggerevents.deleted', $delete);
|
||||
}
|
||||
|
||||
$template = (empty($triggerEvent['settings']['template'])) ? '@MauticPoint/Event/generic.html.twig'
|
||||
: $triggerEvent['settings']['template'];
|
||||
|
||||
// prevent undefined errors
|
||||
$entity = new TriggerEvent();
|
||||
$blank = $entity->convertToArray();
|
||||
$triggerEvent = array_merge($blank, $triggerEvent);
|
||||
|
||||
$dataArray = [
|
||||
'mauticContent' => 'pointTriggerEvent',
|
||||
'success' => 1,
|
||||
'target' => '#triggerEvent'.$objectId,
|
||||
'route' => false,
|
||||
'eventId' => $objectId,
|
||||
'eventHtml' => $this->renderView($template, [
|
||||
'event' => $triggerEvent,
|
||||
'id' => $objectId,
|
||||
'deleted' => true,
|
||||
'sessionId' => $triggerId,
|
||||
]),
|
||||
];
|
||||
} else {
|
||||
$dataArray = ['success' => 0];
|
||||
}
|
||||
|
||||
return new JsonResponse($dataArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Undeletes the entity.
|
||||
*
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function undeleteAction(Request $request, $objectId)
|
||||
{
|
||||
$session = $request->getSession();
|
||||
$triggerId = $request->get('triggerId');
|
||||
$events = $session->get('mautic.point.'.$triggerId.'.triggerevents.modified', []);
|
||||
$delete = $session->get('mautic.point.'.$triggerId.'.triggerevents.deleted', []);
|
||||
|
||||
// ajax only for form fields
|
||||
if (!$request->isXmlHttpRequest()
|
||||
|| !$this->security->isGranted([
|
||||
'point:triggers:edit',
|
||||
'point:triggers:create',
|
||||
], 'MATCH_ONE')
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$triggerEvent = (array_key_exists($objectId, $events)) ? $events[$objectId] : null;
|
||||
|
||||
if ('POST' === $request->getMethod() && null !== $triggerEvent) {
|
||||
// add the field to the delete list
|
||||
if (in_array($objectId, $delete)) {
|
||||
$key = array_search($objectId, $delete);
|
||||
unset($delete[$key]);
|
||||
$session->set('mautic.point.'.$triggerId.'.triggerevents.deleted', $delete);
|
||||
}
|
||||
|
||||
$template = (empty($triggerEvent['settings']['template'])) ? '@MauticPoint/Event/generic.html.twig'
|
||||
: $triggerEvent['settings']['template'];
|
||||
|
||||
// prevent undefined errors
|
||||
$entity = new TriggerEvent();
|
||||
$blank = $entity->convertToArray();
|
||||
$triggerEvent = array_merge($blank, $triggerEvent);
|
||||
|
||||
$dataArray = [
|
||||
'mauticContent' => 'pointTriggerEvent',
|
||||
'success' => 1,
|
||||
'target' => '#triggerEvent'.$objectId,
|
||||
'route' => false,
|
||||
'eventId' => $objectId,
|
||||
'eventHtml' => $this->renderView($template, [
|
||||
'event' => $triggerEvent,
|
||||
'id' => $objectId,
|
||||
'deleted' => false,
|
||||
'triggerId' => $triggerId,
|
||||
]),
|
||||
];
|
||||
} else {
|
||||
$dataArray = ['success' => 0];
|
||||
}
|
||||
|
||||
return new JsonResponse($dataArray);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\Extension;
|
||||
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
|
||||
|
||||
class MauticPointExtension extends Extension
|
||||
{
|
||||
/**
|
||||
* @param mixed[] $configs
|
||||
*/
|
||||
public function load(array $configs, ContainerBuilder $container): void
|
||||
{
|
||||
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Config'));
|
||||
$loader->load('services.php');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\CoreBundle\Entity\UuidInterface;
|
||||
use Mautic\CoreBundle\Entity\UuidTrait;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
class Group extends FormEntity implements UuidInterface
|
||||
{
|
||||
use UuidTrait;
|
||||
|
||||
public const TABLE_NAME = 'point_groups';
|
||||
public const ENTITY_NAME = 'point_group';
|
||||
|
||||
private ?int $id = null;
|
||||
|
||||
private ?string $name = '';
|
||||
|
||||
private ?string $description = '';
|
||||
|
||||
/**
|
||||
* @param ORM\ClassMetadata<Group> $metadata
|
||||
*/
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(GroupRepository::class);
|
||||
|
||||
static::addUuidField($builder);
|
||||
|
||||
$builder->addIdColumns();
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint('name', new Assert\NotBlank([
|
||||
'message' => 'mautic.core.name.required',
|
||||
]));
|
||||
}
|
||||
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('pointGroup')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'name',
|
||||
'description',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'id',
|
||||
'name',
|
||||
'description',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setDescription(?string $description): self
|
||||
{
|
||||
$this->isChanged('description', $description);
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setName(?string $name): self
|
||||
{
|
||||
$this->isChanged('name', $name);
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\CommonEntity;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
|
||||
class GroupContactScore extends CommonEntity
|
||||
{
|
||||
public const TABLE_NAME = 'point_group_contact_score';
|
||||
|
||||
private Lead $contact;
|
||||
|
||||
private Group $group;
|
||||
|
||||
private int $score;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->contact = new Lead();
|
||||
$this->group = new Group();
|
||||
$this->score = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ORM\ClassMetadata<GroupContactScore> $metadata
|
||||
*/
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(GroupContactScoreRepository::class);
|
||||
|
||||
$builder->addContact(false, 'CASCADE', true, 'groupScores');
|
||||
|
||||
$builder->createManyToOne('group', Group::class)
|
||||
->isPrimaryKey()
|
||||
->addJoinColumn('group_id', 'id', true, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->createField('score', Types::INTEGER)
|
||||
->build();
|
||||
}
|
||||
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('groupContactScore')
|
||||
->addListProperties(
|
||||
[
|
||||
'score',
|
||||
'group',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'score',
|
||||
'group',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
public function getContact(): Lead
|
||||
{
|
||||
return $this->contact;
|
||||
}
|
||||
|
||||
public function setContact(Lead $contact): void
|
||||
{
|
||||
$this->contact = $contact;
|
||||
}
|
||||
|
||||
public function getGroup(): Group
|
||||
{
|
||||
return $this->group;
|
||||
}
|
||||
|
||||
public function setGroup(Group $group): void
|
||||
{
|
||||
$this->group = $group;
|
||||
}
|
||||
|
||||
public function getScore(): int
|
||||
{
|
||||
return $this->score;
|
||||
}
|
||||
|
||||
public function setScore(int $score): void
|
||||
{
|
||||
$this->score = $score;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<GroupContactScore>
|
||||
*/
|
||||
class GroupContactScoreRepository extends CommonRepository
|
||||
{
|
||||
public function compareScore(int $leadId, int $groupId, int $score, string $operatorExpr): bool
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('lcs.contact_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.GroupContactScore::TABLE_NAME, 'lcs');
|
||||
|
||||
$expr = $q->expr()->and(
|
||||
$q->expr()->eq('lcs.contact_id', ':lead'),
|
||||
$q->expr()->eq('lcs.group_id', ':groupId'),
|
||||
$q->expr()->$operatorExpr('lcs.score', ':score'),
|
||||
);
|
||||
|
||||
$q->where($expr)
|
||||
->setParameter('lead', $leadId)
|
||||
->setParameter('groupId', $groupId)
|
||||
->setParameter('score', $score);
|
||||
|
||||
return false !== $q->executeQuery()->fetchOne();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Group>
|
||||
*/
|
||||
class GroupRepository extends CommonRepository
|
||||
{
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'pl';
|
||||
}
|
||||
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
// Without qb it returns entities indexed by id instead of array indexes
|
||||
$args['qb'] = $this->createQueryBuilder($this->getTableAlias());
|
||||
|
||||
return parent::getEntities($args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
|
||||
class LeadPointLog
|
||||
{
|
||||
public const TABLE_NAME = 'point_lead_action_log';
|
||||
/**
|
||||
* @var Point
|
||||
**/
|
||||
private $point;
|
||||
|
||||
/**
|
||||
* @var \Mautic\LeadBundle\Entity\Lead
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var IpAddress|null
|
||||
*/
|
||||
private $ipAddress;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
**/
|
||||
private $dateFired;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(LeadPointLogRepository::class);
|
||||
|
||||
$builder->createManyToOne('point', 'Point')
|
||||
->isPrimaryKey()
|
||||
->addJoinColumn('point_id', 'id', true, false, 'CASCADE')
|
||||
->inversedBy('log')
|
||||
->build();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', true);
|
||||
|
||||
$builder->addIpAddress(true);
|
||||
|
||||
$builder->createField('dateFired', 'datetime')
|
||||
->columnName('date_fired')
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDateFired()
|
||||
{
|
||||
return $this->dateFired;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $dateFired
|
||||
*/
|
||||
public function setDateFired($dateFired): void
|
||||
{
|
||||
$this->dateFired = $dateFired;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IpAddress|null
|
||||
*/
|
||||
public function getIpAddress()
|
||||
{
|
||||
return $this->ipAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IpAddress $ipAddress
|
||||
*/
|
||||
public function setIpAddress($ipAddress): void
|
||||
{
|
||||
$this->ipAddress = $ipAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $lead
|
||||
*/
|
||||
public function setLead($lead): void
|
||||
{
|
||||
$this->lead = $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPoint()
|
||||
{
|
||||
return $this->point;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $point
|
||||
*/
|
||||
public function setPoint($point): void
|
||||
{
|
||||
$this->point = $point;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<LeadPointLog>
|
||||
*/
|
||||
class LeadPointLogRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* Updates lead ID (e.g. after a lead merge).
|
||||
*/
|
||||
public function updateLead($fromLeadId, $toLeadId): void
|
||||
{
|
||||
// First check to ensure the $toLead doesn't already exist
|
||||
$results = $this->_em->getConnection()->createQueryBuilder()
|
||||
->select('pl.point_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'point_lead_action_log', 'pl')
|
||||
->where('pl.lead_id = '.$toLeadId)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$actions = [];
|
||||
foreach ($results as $r) {
|
||||
$actions[] = $r['point_id'];
|
||||
}
|
||||
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'point_lead_action_log')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId);
|
||||
|
||||
if (!empty($actions)) {
|
||||
$q->andWhere(
|
||||
$q->expr()->notIn('point_id', $actions)
|
||||
)->executeStatement();
|
||||
|
||||
// Delete remaining leads as the new lead already belongs
|
||||
$this->_em->getConnection()->createQueryBuilder()
|
||||
->delete(MAUTIC_TABLE_PREFIX.'point_lead_action_log')
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
} else {
|
||||
$q->executeStatement();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
|
||||
class LeadTriggerLog
|
||||
{
|
||||
public const TABLE_NAME = 'point_lead_event_log';
|
||||
/**
|
||||
* @var TriggerEvent
|
||||
**/
|
||||
private $event;
|
||||
|
||||
/**
|
||||
* @var \Mautic\LeadBundle\Entity\Lead
|
||||
**/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var IpAddress|null
|
||||
**/
|
||||
private $ipAddress;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
**/
|
||||
private $dateFired;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(LeadTriggerLogRepository::class);
|
||||
|
||||
$builder->createManyToOne('event', 'TriggerEvent')
|
||||
->isPrimaryKey()
|
||||
->addJoinColumn('event_id', 'id', false, false, 'CASCADE')
|
||||
->inversedBy('log')
|
||||
->build();
|
||||
|
||||
$builder->addLead(false, 'CASCADE', true);
|
||||
|
||||
$builder->addIpAddress(true);
|
||||
|
||||
$builder->createField('dateFired', 'datetime')
|
||||
->columnName('date_fired')
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDateFired()
|
||||
{
|
||||
return $this->dateFired;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $dateFired
|
||||
*/
|
||||
public function setDateFired($dateFired): void
|
||||
{
|
||||
$this->dateFired = $dateFired;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IpAddress|null
|
||||
*/
|
||||
public function getIpAddress()
|
||||
{
|
||||
return $this->ipAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IpAddress $ipAddress
|
||||
*/
|
||||
public function setIpAddress($ipAddress): void
|
||||
{
|
||||
$this->ipAddress = $ipAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $lead
|
||||
*/
|
||||
public function setLead($lead): void
|
||||
{
|
||||
$this->lead = $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getEvent()
|
||||
{
|
||||
return $this->event;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $event
|
||||
*/
|
||||
public function setEvent($event): void
|
||||
{
|
||||
$this->event = $event;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<LeadTriggerLog>
|
||||
*/
|
||||
class LeadTriggerLogRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* Updates lead ID (e.g. after a lead merge).
|
||||
*/
|
||||
public function updateLead($fromLeadId, $toLeadId): void
|
||||
{
|
||||
// First check to ensure the $toLead doesn't already exist
|
||||
$results = $this->_em->getConnection()->createQueryBuilder()
|
||||
->select('pl.event_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'point_lead_event_log', 'pl')
|
||||
->where('pl.lead_id = '.$toLeadId)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$events = [];
|
||||
foreach ($results as $r) {
|
||||
$events[] = $r['event_id'];
|
||||
}
|
||||
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'point_lead_event_log')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId);
|
||||
|
||||
if (!empty($events)) {
|
||||
$q->andWhere(
|
||||
$q->expr()->notIn('event_id', $events)
|
||||
)->executeStatement();
|
||||
|
||||
// Delete remaining leads as the new lead already belongs
|
||||
$this->_em->getConnection()->createQueryBuilder()
|
||||
->delete(MAUTIC_TABLE_PREFIX.'point_lead_event_log')
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
} else {
|
||||
$q->executeStatement();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\CoreBundle\Entity\UuidInterface;
|
||||
use Mautic\CoreBundle\Entity\UuidTrait;
|
||||
use Mautic\CoreBundle\Helper\IntHelper;
|
||||
use Mautic\ProjectBundle\Entity\ProjectTrait;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(security: "is_granted('point:triggers:viewown')"),
|
||||
new Post(security: "is_granted('point:triggers:create')"),
|
||||
new Get(security: "is_granted('point:triggers:viewown')"),
|
||||
new Put(security: "is_granted('point:triggers:editown')"),
|
||||
new Patch(security: "is_granted('point:triggers:editother')"),
|
||||
new Delete(security: "is_granted('point:triggers:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['point:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
'api_included' => ['category'],
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['point:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class Point extends FormEntity implements UuidInterface
|
||||
{
|
||||
use UuidTrait;
|
||||
use ProjectTrait;
|
||||
public const ENTITY_NAME = 'point';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['point:read'])]
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['point:read', 'point:write'])]
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['point:read', 'point:write'])]
|
||||
private $description;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['point:read', 'point:write'])]
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['point:read', 'point:write'])]
|
||||
private $repeatable = false;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
#[Groups(['point:read', 'point:write'])]
|
||||
private $publishUp;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
#[Groups(['point:read', 'point:write'])]
|
||||
private $publishDown;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['point:read', 'point:write'])]
|
||||
private $delta = 0;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
#[Groups(['point:read', 'point:write'])]
|
||||
private $properties = [];
|
||||
|
||||
/**
|
||||
* @var ArrayCollection<int,LeadPointLog>
|
||||
*/
|
||||
private $log;
|
||||
|
||||
/**
|
||||
* @var Category|null
|
||||
**/
|
||||
#[Groups(['point:read', 'point:write'])]
|
||||
private $category;
|
||||
|
||||
#[Groups(['point:read', 'point:write'])]
|
||||
private ?Group $group = null;
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->id = null;
|
||||
|
||||
parent::__clone();
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->log = new ArrayCollection();
|
||||
$this->initializeProjects();
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('points')
|
||||
->setCustomRepositoryClass(PointRepository::class)
|
||||
->addIndex(['type'], 'point_type_search');
|
||||
|
||||
$builder->addIdColumns();
|
||||
|
||||
$builder->createField('type', 'string')
|
||||
->length(50)
|
||||
->build();
|
||||
|
||||
$builder->addPublishDates();
|
||||
|
||||
$builder->createField('repeatable', 'boolean')
|
||||
->build();
|
||||
|
||||
$builder->addField('delta', 'integer');
|
||||
|
||||
$builder->addField('properties', 'array');
|
||||
|
||||
$builder->createOneToMany('log', 'LeadPointLog')
|
||||
->mappedBy('point')
|
||||
->cascadePersist()
|
||||
->cascadeRemove()
|
||||
->fetchExtraLazy()
|
||||
->build();
|
||||
|
||||
$builder->addCategory();
|
||||
|
||||
$builder->createManyToOne('group', Group::class)
|
||||
->addJoinColumn('group_id', 'id', true, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
static::addUuidField($builder);
|
||||
self::addProjectsField($builder, 'point_projects_xref', 'point_id');
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint('name', new Assert\NotBlank([
|
||||
'message' => 'mautic.core.name.required',
|
||||
]));
|
||||
|
||||
$metadata->addPropertyConstraint('type', new Assert\NotBlank([
|
||||
'message' => 'mautic.point.type.notblank',
|
||||
]));
|
||||
|
||||
$metadata->addPropertyConstraint('delta', new Assert\NotBlank([
|
||||
'message' => 'mautic.point.delta.notblank',
|
||||
]));
|
||||
|
||||
$metadata->addPropertyConstraint('delta', new Assert\Range([
|
||||
'min' => IntHelper::MIN_INTEGER_VALUE,
|
||||
'max' => IntHelper::MAX_INTEGER_VALUE,
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('point')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'name',
|
||||
'category',
|
||||
'type',
|
||||
'description',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'publishUp',
|
||||
'publishDown',
|
||||
'delta',
|
||||
'properties',
|
||||
'repeatable',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
|
||||
self::addProjectsInLoadApiMetadata($metadata, 'point');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $properties
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setProperties($properties)
|
||||
{
|
||||
$this->isChanged('properties', $properties);
|
||||
|
||||
$this->properties = $properties;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getProperties()
|
||||
{
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->isChanged('type', $type);
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function convertToArray(): array
|
||||
{
|
||||
return get_object_vars($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $description
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
$this->isChanged('description', $description);
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->isChanged('name', $name);
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self
|
||||
*/
|
||||
public function addLog(LeadPointLog $log)
|
||||
{
|
||||
$this->log[] = $log;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeLog(LeadPointLog $log): void
|
||||
{
|
||||
$this->log->removeElement($log);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Doctrine\Common\Collections\Collection
|
||||
*/
|
||||
public function getLog()
|
||||
{
|
||||
return $this->log;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $publishUp
|
||||
*
|
||||
* @return Point
|
||||
*/
|
||||
public function setPublishUp($publishUp)
|
||||
{
|
||||
$this->isChanged('publishUp', $publishUp);
|
||||
$this->publishUp = $publishUp;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getPublishUp()
|
||||
{
|
||||
return $this->publishUp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $publishDown
|
||||
*
|
||||
* @return Point
|
||||
*/
|
||||
public function setPublishDown($publishDown)
|
||||
{
|
||||
$this->isChanged('publishDown', $publishDown);
|
||||
$this->publishDown = $publishDown;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getPublishDown()
|
||||
{
|
||||
return $this->publishDown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCategory()
|
||||
{
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $category
|
||||
*/
|
||||
public function setCategory($category): void
|
||||
{
|
||||
$this->category = $category;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDelta()
|
||||
{
|
||||
return $this->delta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $delta
|
||||
*/
|
||||
public function setDelta($delta): void
|
||||
{
|
||||
$this->delta = (int) $delta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $repeatable
|
||||
*
|
||||
* @return Point
|
||||
*/
|
||||
public function setRepeatable($repeatable)
|
||||
{
|
||||
$this->isChanged('repeatable', $repeatable);
|
||||
$this->repeatable = $repeatable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getRepeatable()
|
||||
{
|
||||
return $this->repeatable;
|
||||
}
|
||||
|
||||
public function getGroup(): ?Group
|
||||
{
|
||||
return $this->group;
|
||||
}
|
||||
|
||||
public function setGroup(?Group $group): void
|
||||
{
|
||||
$this->group = $group;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\ProjectBundle\Entity\ProjectRepositoryTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Point>
|
||||
*/
|
||||
class PointRepository extends CommonRepository
|
||||
{
|
||||
use ProjectRepositoryTrait;
|
||||
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$q = $this->_em
|
||||
->createQueryBuilder()
|
||||
->select($this->getTableAlias().', cat')
|
||||
->from(Point::class, $this->getTableAlias())
|
||||
->leftJoin($this->getTableAlias().'.category', 'cat')
|
||||
->leftJoin($this->getTableAlias().'.group', 'pl');
|
||||
|
||||
$args['qb'] = $q;
|
||||
|
||||
return parent::getEntities($args);
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'p';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of published actions based on type.
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPublishedByType($type)
|
||||
{
|
||||
$q = $this->createQueryBuilder('p')
|
||||
->select('partial p.{id, type, name, delta, repeatable, properties}')
|
||||
->setParameter('type', $type);
|
||||
|
||||
// make sure the published up and down dates are good
|
||||
$expr = $this->getPublishedByDateExpression($q);
|
||||
$expr->add($q->expr()->eq('p.type', ':type'));
|
||||
|
||||
$q->where($expr);
|
||||
|
||||
return $q->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param int $leadId
|
||||
*/
|
||||
public function getCompletedLeadActions($type, $leadId): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder()
|
||||
->select('p.*')
|
||||
->from(MAUTIC_TABLE_PREFIX.'point_lead_action_log', 'x')
|
||||
->innerJoin('x', MAUTIC_TABLE_PREFIX.'points', 'p', 'x.point_id = p.id');
|
||||
|
||||
// make sure the published up and down dates are good
|
||||
$q->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('p.type', ':type'),
|
||||
$q->expr()->eq('x.lead_id', (int) $leadId)
|
||||
)
|
||||
)
|
||||
->setParameter('type', $type);
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$return = [];
|
||||
|
||||
foreach ($results as $r) {
|
||||
$return[$r['id']] = $r;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $leadId
|
||||
*/
|
||||
public function getCompletedLeadActionsByLeadId($leadId): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder()
|
||||
->select('p.*')
|
||||
->from(MAUTIC_TABLE_PREFIX.'point_lead_action_log', 'x')
|
||||
->innerJoin('x', MAUTIC_TABLE_PREFIX.'points', 'p', 'x.point_id = p.id');
|
||||
|
||||
// make sure the published up and down dates are good
|
||||
$q->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('x.lead_id', (int) $leadId)
|
||||
)
|
||||
);
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$return = [];
|
||||
|
||||
foreach ($results as $r) {
|
||||
$return[$r['id']] = $r;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
protected function addCatchAllWhereClause($q, $filter): array
|
||||
{
|
||||
return $this->addStandardCatchAllWhereClause($q, $filter, [
|
||||
'p.name',
|
||||
'p.description',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
return match ($filter->command) {
|
||||
$this->translator->trans('mautic.project.searchcommand.name'), $this->translator->trans('mautic.project.searchcommand.name', [], null, 'en_US') => $this->handleProjectFilter(
|
||||
$this->_em->getConnection()->createQueryBuilder(),
|
||||
'point_id',
|
||||
'point_projects_xref',
|
||||
$this->getTableAlias(),
|
||||
$filter->string,
|
||||
$filter->not
|
||||
),
|
||||
default => $this->addStandardSearchCommandWhereClause($q, $filter),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
return array_merge(['mautic.project.searchcommand.name'], $this->getStandardSearchCommands());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\CoreBundle\Entity\UuidInterface;
|
||||
use Mautic\CoreBundle\Entity\UuidTrait;
|
||||
use Mautic\ProjectBundle\Entity\ProjectTrait;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(security: "is_granted('point:triggers:viewown')"),
|
||||
new Post(security: "is_granted('point:triggers:create')"),
|
||||
new Get(security: "is_granted('point:triggers:viewown')"),
|
||||
new Put(security: "is_granted('point:triggers:editown')"),
|
||||
new Patch(security: "is_granted('point:triggers:editother')"),
|
||||
new Delete(security: "is_granted('point:triggers:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['trigger:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
'api_included' => ['category', 'events'],
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['trigger:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class Trigger extends FormEntity implements UuidInterface
|
||||
{
|
||||
use UuidTrait;
|
||||
use ProjectTrait;
|
||||
public const ENTITY_NAME = 'point_trigger';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['trigger:read'])]
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['trigger:read', 'trigger:write'])]
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['trigger:read', 'trigger:write'])]
|
||||
private $description;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
#[Groups(['trigger:read', 'trigger:write'])]
|
||||
private $publishUp;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
#[Groups(['trigger:read', 'trigger:write'])]
|
||||
private $publishDown;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['trigger:read', 'trigger:write'])]
|
||||
private $points = 0;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['trigger:read', 'trigger:write'])]
|
||||
private $color = 'a0acb8';
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['trigger:read', 'trigger:write'])]
|
||||
private $triggerExistingLeads = false;
|
||||
|
||||
/**
|
||||
* @var Category|null
|
||||
**/
|
||||
#[Groups(['trigger:read', 'trigger:write'])]
|
||||
private $category;
|
||||
|
||||
/**
|
||||
* @var ArrayCollection<int, TriggerEvent>
|
||||
*/
|
||||
#[Groups(['trigger:read', 'trigger:write'])]
|
||||
private $events;
|
||||
|
||||
#[Groups(['trigger:read', 'trigger:write'])]
|
||||
private ?Group $group = null;
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->id = null;
|
||||
|
||||
parent::__clone();
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->events = new ArrayCollection();
|
||||
$this->initializeProjects();
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('point_triggers')
|
||||
->setCustomRepositoryClass(TriggerRepository::class);
|
||||
|
||||
$builder->addIdColumns();
|
||||
|
||||
$builder->addPublishDates();
|
||||
|
||||
$builder->addField('points', 'integer');
|
||||
|
||||
$builder->createField('color', 'string')
|
||||
->length(7)
|
||||
->build();
|
||||
|
||||
$builder->createField('triggerExistingLeads', 'boolean')
|
||||
->columnName('trigger_existing_leads')
|
||||
->build();
|
||||
|
||||
$builder->addCategory();
|
||||
|
||||
$builder->createOneToMany('events', 'TriggerEvent')
|
||||
->setIndexBy('id')
|
||||
->setOrderBy(['order' => 'ASC'])
|
||||
->mappedBy('trigger')
|
||||
->cascadeAll()
|
||||
->fetchExtraLazy()
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('group', Group::class)
|
||||
->addJoinColumn('group_id', 'id', true, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
static::addUuidField($builder);
|
||||
self::addProjectsField($builder, 'point_trigger_projects_xref', 'point_trigger_id');
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint('name', new Assert\NotBlank([
|
||||
'message' => 'mautic.core.name.required',
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('trigger')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'name',
|
||||
'category',
|
||||
'description',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'publishUp',
|
||||
'publishDown',
|
||||
'points',
|
||||
'color',
|
||||
'events',
|
||||
'triggerExistingLeads',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
|
||||
self::addProjectsInLoadApiMetadata($metadata, 'trigger');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $prop
|
||||
* @param mixed $val
|
||||
*/
|
||||
protected function isChanged($prop, $val)
|
||||
{
|
||||
if ('events' == $prop) {
|
||||
// changes are already computed so just add them
|
||||
$this->changes[$prop][$val[0]] = $val[1];
|
||||
} else {
|
||||
parent::isChanged($prop, $val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set description.
|
||||
*
|
||||
* @param string $description
|
||||
*
|
||||
* @return Trigger
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
$this->isChanged('description', $description);
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get description.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set name.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return Trigger
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->isChanged('name', $name);
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add events.
|
||||
*
|
||||
* @return Point
|
||||
*/
|
||||
public function addTriggerEvent($key, TriggerEvent $event)
|
||||
{
|
||||
if ($changes = $event->getChanges()) {
|
||||
$this->isChanged('events', [$key, $changes]);
|
||||
}
|
||||
$this->events[$key] = $event;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove events.
|
||||
*/
|
||||
public function removeTriggerEvent(TriggerEvent $event): void
|
||||
{
|
||||
$this->events->removeElement($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events.
|
||||
*
|
||||
* @return \Doctrine\Common\Collections\Collection
|
||||
*/
|
||||
public function getEvents()
|
||||
{
|
||||
return $this->events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set publishUp.
|
||||
*
|
||||
* @param \DateTime $publishUp
|
||||
*
|
||||
* @return Point
|
||||
*/
|
||||
public function setPublishUp($publishUp)
|
||||
{
|
||||
$this->isChanged('publishUp', $publishUp);
|
||||
$this->publishUp = $publishUp;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get publishUp.
|
||||
*
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getPublishUp()
|
||||
{
|
||||
return $this->publishUp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set publishDown.
|
||||
*
|
||||
* @param \DateTime $publishDown
|
||||
*
|
||||
* @return Point
|
||||
*/
|
||||
public function setPublishDown($publishDown)
|
||||
{
|
||||
$this->isChanged('publishDown', $publishDown);
|
||||
$this->publishDown = $publishDown;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get publishDown.
|
||||
*
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getPublishDown()
|
||||
{
|
||||
return $this->publishDown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPoints()
|
||||
{
|
||||
return $this->points;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $points
|
||||
*/
|
||||
public function setPoints($points): void
|
||||
{
|
||||
$this->isChanged('points', $points);
|
||||
$this->points = $points;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getColor()
|
||||
{
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $color
|
||||
*/
|
||||
public function setColor($color): void
|
||||
{
|
||||
$this->color = $color;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getTriggerExistingLeads()
|
||||
{
|
||||
return $this->triggerExistingLeads;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $triggerExistingLeads
|
||||
*/
|
||||
public function setTriggerExistingLeads($triggerExistingLeads): void
|
||||
{
|
||||
$this->triggerExistingLeads = $triggerExistingLeads;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCategory()
|
||||
{
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $category
|
||||
*/
|
||||
public function setCategory($category): void
|
||||
{
|
||||
$this->category = $category;
|
||||
}
|
||||
|
||||
public function getGroup(): ?Group
|
||||
{
|
||||
return $this->group;
|
||||
}
|
||||
|
||||
public function setGroup(Group $group): void
|
||||
{
|
||||
$this->group = $group;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\UuidInterface;
|
||||
use Mautic\CoreBundle\Entity\UuidTrait;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(security: "is_granted('point:triggers:viewown')"),
|
||||
new Post(security: "is_granted('point:triggers:create')"),
|
||||
new Get(security: "is_granted('point:triggers:viewown')"),
|
||||
new Put(security: "is_granted('point:triggers:editown')"),
|
||||
new Patch(security: "is_granted('point:triggers:editother')"),
|
||||
new Delete(security: "is_granted('point:triggers:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['trigger_event:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['trigger_event:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class TriggerEvent implements UuidInterface
|
||||
{
|
||||
use UuidTrait;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
#[Groups(['trigger_event:read'])]
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['trigger_event:read', 'trigger_event:write'])]
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['trigger_event:read', 'trigger_event:write'])]
|
||||
private $description;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['trigger_event:read', 'trigger_event:write'])]
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['trigger_event:read', 'trigger_event:write'])]
|
||||
private $order = 0;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
#[Groups(['trigger_event:read', 'trigger_event:write'])]
|
||||
private $properties = [];
|
||||
|
||||
/**
|
||||
* @var Trigger
|
||||
*/
|
||||
#[Groups(['trigger_event:read', 'trigger_event:write'])]
|
||||
private $trigger;
|
||||
|
||||
/**
|
||||
* @var ArrayCollection<int,LeadTriggerLog>
|
||||
*/
|
||||
private $log;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $changes;
|
||||
|
||||
public function __clone(): void
|
||||
{
|
||||
$this->id = null;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->log = new ArrayCollection();
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('point_trigger_events')
|
||||
->setCustomRepositoryClass(TriggerEventRepository::class)
|
||||
->addIndex(['type'], 'trigger_type_search');
|
||||
|
||||
$builder->addIdColumns();
|
||||
|
||||
$builder->createField('type', 'string')
|
||||
->length(50)
|
||||
->build();
|
||||
|
||||
$builder->createField('order', 'integer')
|
||||
->columnName('action_order')
|
||||
->build();
|
||||
|
||||
$builder->addField('properties', 'array');
|
||||
|
||||
$builder->createManyToOne('trigger', 'Trigger')
|
||||
->inversedBy('events')
|
||||
->addJoinColumn('trigger_id', 'id', false, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->createOneToMany('log', 'LeadTriggerLog')
|
||||
->mappedBy('event')
|
||||
->cascadePersist()
|
||||
->cascadeRemove()
|
||||
->fetchExtraLazy()
|
||||
->build();
|
||||
|
||||
static::addUuidField($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('trigger')
|
||||
->addProperties(
|
||||
[
|
||||
'id',
|
||||
'name',
|
||||
'description',
|
||||
'type',
|
||||
'order',
|
||||
'properties',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
private function isChanged($prop, $val): void
|
||||
{
|
||||
if ($this->$prop != $val) {
|
||||
$this->changes[$prop] = [$this->$prop, $val];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getChanges()
|
||||
{
|
||||
return $this->changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $order
|
||||
*
|
||||
* @return TriggerEvent
|
||||
*/
|
||||
public function setOrder($order)
|
||||
{
|
||||
$this->isChanged('order', $order);
|
||||
|
||||
$this->order = $order;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getOrder()
|
||||
{
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $properties
|
||||
*
|
||||
* @return TriggerEvent
|
||||
*/
|
||||
public function setProperties($properties)
|
||||
{
|
||||
$this->isChanged('properties', $properties);
|
||||
|
||||
$this->properties = $properties;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getProperties()
|
||||
{
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self
|
||||
*/
|
||||
public function setTrigger(Trigger $trigger)
|
||||
{
|
||||
$this->trigger = $trigger;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Trigger
|
||||
*/
|
||||
public function getTrigger()
|
||||
{
|
||||
return $this->trigger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
*
|
||||
* @return TriggerEvent
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
$this->isChanged('type', $type);
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function convertToArray(): array
|
||||
{
|
||||
return get_object_vars($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $description
|
||||
*
|
||||
* @return TriggerEvent
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
$this->isChanged('description', $description);
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return TriggerEvent
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->isChanged('name', $name);
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self
|
||||
*/
|
||||
public function addLog(LeadTriggerLog $log)
|
||||
{
|
||||
$this->log[] = $log;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeLog(LeadTriggerLog $log): void
|
||||
{
|
||||
$this->log->removeElement($log);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Doctrine\Common\Collections\Collection
|
||||
*/
|
||||
public function getLog()
|
||||
{
|
||||
return $this->log;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<TriggerEvent>
|
||||
*/
|
||||
class TriggerEventRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* Get array of published triggers based on point total.
|
||||
*
|
||||
* @param int $points
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPublishedByPointTotal($points)
|
||||
{
|
||||
$q = $this->createQueryBuilder('a')
|
||||
->select('partial a.{id, type, name, properties}, partial r.{id, name, points, color}')
|
||||
->leftJoin('a.trigger', 'r')
|
||||
->orderBy('a.order,r.points');
|
||||
|
||||
// make sure the published up and down dates are good
|
||||
$expr = $this->getPublishedByDateExpression($q, 'r');
|
||||
|
||||
$expr->add(
|
||||
$q->expr()->lte('r.points', (int) $points)
|
||||
);
|
||||
|
||||
$q->where($expr);
|
||||
$q->andWhere('r.group IS NULL');
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ArrayCollection<int,GroupContactScore> $groupScores
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getPublishedByGroupScore(Collection $groupScores)
|
||||
{
|
||||
if ($groupScores->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$q = $this->createQueryBuilder('a')
|
||||
->select('partial a.{id, type, name, properties}, partial r.{id, name, points, color}, partial pl.{id, name}')
|
||||
->leftJoin('a.trigger', 'r')
|
||||
->leftJoin('r.group', 'pl')
|
||||
->orderBy('a.order');
|
||||
|
||||
// make sure the published up and down dates are good
|
||||
$expr = $this->getPublishedByDateExpression($q, 'r');
|
||||
|
||||
$groupsExpr = $q->expr()->orX();
|
||||
/** @var GroupContactScore $score */
|
||||
foreach ($groupScores as $score) {
|
||||
$groupsExpr->add(
|
||||
$q->expr()->andX(
|
||||
$q->expr()->eq('pl.id', $score->getGroup()->getId()),
|
||||
$q->expr()->lte('r.points', $score->getScore())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$q->where($expr);
|
||||
$q->andWhere($groupsExpr);
|
||||
$q->andWhere('r.group IS NOT NULL');
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of published actions based on type.
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPublishedByType($type)
|
||||
{
|
||||
$q = $this->createQueryBuilder('e')
|
||||
->select('partial e.{id, type, name, properties}, partial t.{id, name, points, color}')
|
||||
->join('e.trigger', 't')
|
||||
->orderBy('e.order');
|
||||
|
||||
// make sure the published up and down dates are good
|
||||
$expr = $this->getPublishedByDateExpression($q);
|
||||
$expr->add(
|
||||
$q->expr()->eq('e.type', ':type')
|
||||
);
|
||||
$q->where($expr)
|
||||
->setParameter('type', $type);
|
||||
|
||||
return $q->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $leadId
|
||||
*/
|
||||
public function getLeadTriggeredEvents($leadId): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder()
|
||||
->select('e.*')
|
||||
->from(MAUTIC_TABLE_PREFIX.'point_lead_event_log', 'x')
|
||||
->innerJoin('x', MAUTIC_TABLE_PREFIX.'point_trigger_events', 'e', 'x.event_id = e.id')
|
||||
->innerJoin('e', MAUTIC_TABLE_PREFIX.'point_triggers', 't', 'e.trigger_id = t.id');
|
||||
|
||||
// make sure the published up and down dates are good
|
||||
$q->where($q->expr()->eq('x.lead_id', (int) $leadId));
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$return = [];
|
||||
|
||||
foreach ($results as $r) {
|
||||
$return[$r['id']] = $r;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $eventId
|
||||
*/
|
||||
public function getLeadsForEvent($eventId): array
|
||||
{
|
||||
$results = $this->_em->getConnection()->createQueryBuilder()
|
||||
->select('e.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'point_lead_event_log', 'e')
|
||||
->where('e.event_id = '.(int) $eventId)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$return = [];
|
||||
|
||||
foreach ($results as $r) {
|
||||
$return[] = $r['lead_id'];
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\ProjectBundle\Entity\ProjectRepositoryTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Trigger>
|
||||
*/
|
||||
class TriggerRepository extends CommonRepository
|
||||
{
|
||||
use ProjectRepositoryTrait;
|
||||
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$q = $this->_em
|
||||
->createQueryBuilder()
|
||||
->select($this->getTableAlias().', cat')
|
||||
->from(Trigger::class, $this->getTableAlias())
|
||||
->leftJoin($this->getTableAlias().'.category', 'cat')
|
||||
->leftJoin($this->getTableAlias().'.group', 'pl');
|
||||
|
||||
$args['qb'] = $q;
|
||||
|
||||
return parent::getEntities($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of published triggers with color and points.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getTriggerColors()
|
||||
{
|
||||
$q = $this->_em->createQueryBuilder()
|
||||
->select('partial t.{id, color, points}')
|
||||
->from(Trigger::class, 't', 't.id');
|
||||
|
||||
$q->where($this->getPublishedByDateExpression($q));
|
||||
$q->orderBy('t.points', Order::Ascending->value);
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 't';
|
||||
}
|
||||
|
||||
protected function addCatchAllWhereClause($q, $filter): array
|
||||
{
|
||||
return $this->addStandardCatchAllWhereClause($q, $filter, [
|
||||
't.name',
|
||||
't.description',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
return match ($filter->command) {
|
||||
$this->translator->trans('mautic.project.searchcommand.name'), $this->translator->trans('mautic.project.searchcommand.name', [], null, 'en_US') => $this->handleProjectFilter(
|
||||
$this->_em->getConnection()->createQueryBuilder(),
|
||||
'point_trigger_id',
|
||||
'point_trigger_projects_xref',
|
||||
$this->getTableAlias(),
|
||||
$filter->string,
|
||||
$filter->not
|
||||
),
|
||||
// Handle standard search commands
|
||||
default => $this->addStandardSearchCommandWhereClause($q, $filter),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
return array_merge(['mautic.project.searchcommand.name'], $this->getStandardSearchCommands());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Event;
|
||||
|
||||
use Mautic\PointBundle\Entity\Group;
|
||||
|
||||
final class GroupEvent
|
||||
{
|
||||
public function __construct(
|
||||
private Group $entity,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getGroup(): Group
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
public function setGroup(Group $group): void
|
||||
{
|
||||
$this->entity = $group;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Event;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\PointBundle\Entity\GroupContactScore;
|
||||
|
||||
final class GroupScoreChangeEvent
|
||||
{
|
||||
public function __construct(
|
||||
private GroupContactScore $groupContactScore,
|
||||
private int $oldScore,
|
||||
private int $newScore,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getGroupContactScore(): GroupContactScore
|
||||
{
|
||||
return $this->groupContactScore;
|
||||
}
|
||||
|
||||
public function getContact(): Lead
|
||||
{
|
||||
return $this->groupContactScore->getContact();
|
||||
}
|
||||
|
||||
public function getNewScore(): int
|
||||
{
|
||||
return $this->newScore;
|
||||
}
|
||||
|
||||
public function getOldScore(): int
|
||||
{
|
||||
return $this->oldScore;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\PointBundle\Entity\Point;
|
||||
|
||||
class PointActionEvent extends CommonEvent
|
||||
{
|
||||
public function __construct(
|
||||
protected Point $point,
|
||||
protected Lead $lead,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Point
|
||||
*/
|
||||
public function getPoint()
|
||||
{
|
||||
return $this->point;
|
||||
}
|
||||
|
||||
public function setPoint(Point $point): void
|
||||
{
|
||||
$this->point = $point;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
public function setLead(Lead $lead): void
|
||||
{
|
||||
$this->lead = $lead;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Event;
|
||||
|
||||
use Symfony\Component\Process\Exception\InvalidArgumentException;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class PointBuilderEvent extends Event
|
||||
{
|
||||
private array $actions = [];
|
||||
|
||||
public function __construct(
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an action to the list of available .
|
||||
*
|
||||
* @param string $key - a unique identifier; it is recommended that it be namespaced i.e. lead.action
|
||||
* @param array $action - can contain the following keys:
|
||||
* 'label' => (required) what to display in the list
|
||||
* 'description' => (optional) short description of event
|
||||
* 'template' => (optional) template to use for the action's HTML in the point builder
|
||||
* i.e AcmeMyBundle:PointAction:theaction.html.twig
|
||||
* 'formType' => (optional) name of the form type SERVICE for the action; will use a default form with point change only
|
||||
* 'formTypeOptions' => (optional) array of options to pass to formType
|
||||
* 'callback' => (optional) callback function that will be passed when the action is triggered; return true to
|
||||
* change the configured points or false to ignore the action
|
||||
* The callback function can receive the following arguments by name (via ReflectionMethod::invokeArgs())
|
||||
* Mautic\LeadBundle\Entity\Lead $lead
|
||||
* $eventDetails - variable sent from firing function to call back function
|
||||
* array $action = array(
|
||||
* 'id' => int
|
||||
* 'type' => string
|
||||
* 'name' => string
|
||||
* 'properties' => array()
|
||||
* )
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function addAction($key, array $action): void
|
||||
{
|
||||
if (array_key_exists($key, $this->actions)) {
|
||||
throw new InvalidArgumentException("The key, '$key' is already used by another action. Please use a different key.");
|
||||
}
|
||||
|
||||
// check for required keys and that given functions are callable
|
||||
$this->verifyComponent(
|
||||
['group', 'label'],
|
||||
['callback'],
|
||||
$action
|
||||
);
|
||||
|
||||
// translate the label and group
|
||||
$action['label'] = $this->translator->trans($action['label']);
|
||||
$action['group'] = $this->translator->trans($action['group']);
|
||||
|
||||
$this->actions[$key] = $action;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getActions()
|
||||
{
|
||||
uasort($this->actions, fn ($a, $b): int => strnatcasecmp(
|
||||
$a['label'], $b['label']));
|
||||
|
||||
return $this->actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of actions supported by the choice form field.
|
||||
*/
|
||||
public function getActionList(): array
|
||||
{
|
||||
$list = [];
|
||||
$actions = $this->getActions();
|
||||
foreach ($actions as $k => $a) {
|
||||
$list[$k] = $a['label'];
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getActionChoices(): array
|
||||
{
|
||||
$choices = [];
|
||||
foreach ($this->actions as $k => $c) {
|
||||
$choices[$c['group']][$c['label']] = $k;
|
||||
}
|
||||
|
||||
return $choices;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function verifyComponent(array $keys, array $methods, array $component): void
|
||||
{
|
||||
foreach ($keys as $k) {
|
||||
if (!array_key_exists($k, $component)) {
|
||||
throw new InvalidArgumentException("The key, '$k' is missing.");
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($methods as $m) {
|
||||
if (isset($component[$m]) && !is_callable($component[$m], true)) {
|
||||
throw new InvalidArgumentException($component[$m].' is not callable. Please ensure that it exists and that it is a fully qualified namespace.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
use Mautic\PointBundle\Entity\Point;
|
||||
|
||||
class PointEvent extends CommonEvent
|
||||
{
|
||||
/**
|
||||
* @param bool $isNew
|
||||
*/
|
||||
public function __construct(Point &$point, $isNew = false)
|
||||
{
|
||||
$this->entity = &$point;
|
||||
$this->isNew = $isNew;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Point
|
||||
*/
|
||||
public function getPoint()
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
public function setPoint(Point $point): void
|
||||
{
|
||||
$this->entity = $point;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Event;
|
||||
|
||||
use Symfony\Component\Process\Exception\InvalidArgumentException;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class TriggerBuilderEvent extends Event
|
||||
{
|
||||
private array $events = [];
|
||||
|
||||
public function __construct(
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an action to the list of available .
|
||||
*
|
||||
* @param string $key - a unique identifier; it is recommended that it be namespaced i.e. lead.action
|
||||
* @param array $event - can contain the following keys:
|
||||
* 'label' => (required) what to display in the list
|
||||
* 'description' => (optional) short description of event
|
||||
* 'template' => (optional) template to use for the action's HTML in the point builder
|
||||
* i.e AcmeMyBundle:PointAction:theaction.html.twig
|
||||
* 'formType' => (optional) name of the form type SERVICE for the action
|
||||
* 'formTypeOptions' => (optional) array of options to pass to formType
|
||||
* 'callback' => (required) callback function that will be passed when the action is triggered
|
||||
* The callback function can receive the following arguments by name (via ReflectionMethod::invokeArgs())
|
||||
* Mautic\PointBundle\Entity\TriggerEvent $event
|
||||
* Mautic\LeadBundle\Entity\Lead $lead
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function addEvent($key, array $event): void
|
||||
{
|
||||
if (array_key_exists($key, $this->events)) {
|
||||
throw new InvalidArgumentException("The key, '$key' is already used by another action. Please use a different key.");
|
||||
}
|
||||
|
||||
// check for required keys and that given functions are callable
|
||||
$this->verifyComponent(
|
||||
['group', 'label'],
|
||||
['callback'],
|
||||
$event
|
||||
);
|
||||
|
||||
// Support for old way with callback and new event based system
|
||||
// Could be removed after all events will be refactored to events. The key 'eventName' will be mandatory and 'callback' will be removed.
|
||||
if (!array_key_exists('callback', $event) && !array_key_exists('eventName', $event)) {
|
||||
throw new InvalidArgumentException("One of the 'callback' or 'eventName' has to be provided. Use 'eventName' for new code");
|
||||
}
|
||||
|
||||
$event['label'] = $this->translator->trans($event['label']);
|
||||
$event['group'] = $this->translator->trans($event['group']);
|
||||
$event['description'] = (isset($event['description'])) ? $this->translator->trans($event['description']) : '';
|
||||
|
||||
$this->events[$key] = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getEvents()
|
||||
{
|
||||
uasort($this->events, fn ($a, $b): int => strnatcasecmp(
|
||||
$a['label'], $b['label']));
|
||||
|
||||
return $this->events;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function verifyComponent(array $keys, array $methods, array $component): void
|
||||
{
|
||||
foreach ($keys as $k) {
|
||||
if (!array_key_exists($k, $component)) {
|
||||
throw new InvalidArgumentException("The key, '$k' is missing.");
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($methods as $m) {
|
||||
if (isset($component[$m]) && !is_callable($component[$m], true)) {
|
||||
throw new InvalidArgumentException($component[$m].' is not callable. Please ensure that it exists and that it is a fully qualified namespace.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
use Mautic\PointBundle\Entity\Trigger;
|
||||
|
||||
class TriggerEvent extends CommonEvent
|
||||
{
|
||||
/**
|
||||
* @var Trigger
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* @param bool $isNew
|
||||
*/
|
||||
public function __construct(
|
||||
Trigger &$trigger,
|
||||
protected $isNew = false,
|
||||
) {
|
||||
$this->entity = &$trigger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Trigger
|
||||
*/
|
||||
public function getTrigger()
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
public function setTrigger(Trigger $trigger): void
|
||||
{
|
||||
$this->entity = $trigger;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Event;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\PointBundle\Entity\TriggerEvent as TriggerEventEntity;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class TriggerExecutedEvent extends Event
|
||||
{
|
||||
private ?bool $result = null;
|
||||
|
||||
public function __construct(
|
||||
private TriggerEventEntity $triggerEvent,
|
||||
private Lead $lead,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TriggerEventEntity
|
||||
*/
|
||||
public function getTriggerEvent()
|
||||
{
|
||||
return $this->triggerEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getResult()
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
public function setSucceded(): void
|
||||
{
|
||||
$this->result = true;
|
||||
}
|
||||
|
||||
public function setFailed(): void
|
||||
{
|
||||
$this->result = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\EventListener;
|
||||
|
||||
use Mautic\DashboardBundle\Event\WidgetDetailEvent;
|
||||
use Mautic\DashboardBundle\EventListener\DashboardSubscriber as MainDashboardSubscriber;
|
||||
use Mautic\PointBundle\Model\PointModel;
|
||||
|
||||
class DashboardSubscriber extends MainDashboardSubscriber
|
||||
{
|
||||
/**
|
||||
* Define the name of the bundle/category of the widget(s).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bundle = 'point';
|
||||
|
||||
/**
|
||||
* Define the widget(s).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $types = [
|
||||
'points.in.time' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* Define permissions to see those widgets.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'point:points:viewown',
|
||||
'point:points:viewother',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected PointModel $pointModel,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a widget detail when needed.
|
||||
*/
|
||||
public function onWidgetDetailGenerate(WidgetDetailEvent $event): void
|
||||
{
|
||||
$this->checkPermissions($event);
|
||||
$canViewOthers = $event->hasPermission('point:points:viewother');
|
||||
|
||||
if ('points.in.time' == $event->getType()) {
|
||||
$widget = $event->getWidget();
|
||||
$params = $widget->getParams();
|
||||
|
||||
if (!$event->isCached()) {
|
||||
$event->setTemplateData([
|
||||
'chartType' => 'line',
|
||||
'chartHeight' => $widget->getHeight() - 80,
|
||||
'chartData' => $this->pointModel->getPointLineChartData(
|
||||
$params['timeUnit'],
|
||||
$params['dateFrom'],
|
||||
$params['dateTo'],
|
||||
$params['dateFormat'],
|
||||
[],
|
||||
$canViewOthers
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
$event->setTemplate('@MauticCore/Helper/chart.html.twig');
|
||||
$event->stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\EventListener;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\CoreBundle\Event\EntityExportEvent;
|
||||
use Mautic\CoreBundle\Event\EntityImportAnalyzeEvent;
|
||||
use Mautic\CoreBundle\Event\EntityImportEvent;
|
||||
use Mautic\CoreBundle\Event\EntityImportUndoEvent;
|
||||
use Mautic\CoreBundle\EventListener\ImportExportTrait;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\PointBundle\Entity\Group;
|
||||
use Mautic\PointBundle\Model\PointGroupModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
|
||||
final class GroupImportExportSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
use ImportExportTrait;
|
||||
|
||||
public function __construct(
|
||||
private PointGroupModel $pointGroupModel,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private AuditLogModel $auditLogModel,
|
||||
private IpLookupHelper $ipLookupHelper,
|
||||
private DenormalizerInterface $serializer,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
EntityExportEvent::class => ['onPointGroupExport', 0],
|
||||
EntityImportEvent::class => ['onPointGroupImport', 0],
|
||||
EntityImportUndoEvent::class => ['onUndoImport', 0],
|
||||
EntityImportAnalyzeEvent::class => ['onDuplicationCheck', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onPointGroupExport(EntityExportEvent $event): void
|
||||
{
|
||||
if (Group::ENTITY_NAME !== $event->getEntityName()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pointGroupId = $event->getEntityId();
|
||||
$pointGroup = $this->pointGroupModel->getEntity($pointGroupId);
|
||||
if (!$pointGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pointGroupData = [
|
||||
'id' => $pointGroup->getId(),
|
||||
'name' => $pointGroup->getName(),
|
||||
'description' => $pointGroup->getDescription(),
|
||||
'is_published'=> $pointGroup->isPublished(),
|
||||
'uuid' => $pointGroup->getUuid(),
|
||||
];
|
||||
|
||||
$event->addEntity(Group::ENTITY_NAME, $pointGroupData);
|
||||
$this->logAction('export', $pointGroup->getId(), $pointGroupData);
|
||||
}
|
||||
|
||||
public function onPointGroupImport(EntityImportEvent $event): void
|
||||
{
|
||||
if (Group::ENTITY_NAME !== $event->getEntityName() || !$event->getEntityData()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stats = [
|
||||
EntityImportEvent::NEW => ['names' => [], 'ids' => [], 'count' => 0],
|
||||
EntityImportEvent::UPDATE => ['names' => [], 'ids' => [], 'count' => 0],
|
||||
];
|
||||
|
||||
foreach ($event->getEntityData() as $element) {
|
||||
$group = $this->entityManager->getRepository(Group::class)->findOneBy(['uuid' => $element['uuid']]);
|
||||
$isNew = !$group;
|
||||
|
||||
$group ??= new Group();
|
||||
$this->serializer->denormalize(
|
||||
$element,
|
||||
Group::class,
|
||||
null,
|
||||
['object_to_populate' => $group]
|
||||
);
|
||||
$this->pointGroupModel->saveEntity($group);
|
||||
|
||||
$event->addEntityIdMap((int) $element['id'], $group->getId());
|
||||
|
||||
$status = $isNew ? EntityImportEvent::NEW : EntityImportEvent::UPDATE;
|
||||
$stats[$status]['names'][] = $group->getName();
|
||||
$stats[$status]['ids'][] = $group->getId();
|
||||
++$stats[$status]['count'];
|
||||
|
||||
$this->logAction('import', $group->getId(), $element);
|
||||
}
|
||||
|
||||
foreach ($stats as $status => $info) {
|
||||
if ($info['count'] > 0) {
|
||||
$event->setStatus($status, [Group::ENTITY_NAME => $info]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onUndoImport(EntityImportUndoEvent $event): void
|
||||
{
|
||||
if (Group::ENTITY_NAME !== $event->getEntityName()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$summary = $event->getSummary();
|
||||
|
||||
if (!isset($summary['ids']) || empty($summary['ids'])) {
|
||||
return;
|
||||
}
|
||||
foreach ($summary['ids'] as $id) {
|
||||
$entity = $this->entityManager->getRepository(Group::class)->find($id);
|
||||
|
||||
if ($entity) {
|
||||
$this->entityManager->remove($entity);
|
||||
$this->logAction('undo_import', $id, ['deletedEntity' => Group::class]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function onDuplicationCheck(EntityImportAnalyzeEvent $event): void
|
||||
{
|
||||
$this->performDuplicationCheck(
|
||||
$event,
|
||||
Group::ENTITY_NAME,
|
||||
Group::class,
|
||||
'name',
|
||||
$this->entityManager
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $details
|
||||
*/
|
||||
private function logAction(string $action, int $objectId, array $details): void
|
||||
{
|
||||
$this->auditLogModel->writeToLog([
|
||||
'bundle' => 'point',
|
||||
'object' => 'pointGroup',
|
||||
'objectId' => $objectId,
|
||||
'action' => $action,
|
||||
'details' => $details,
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\EventListener;
|
||||
|
||||
use Mautic\PointBundle\Event\GroupScoreChangeEvent;
|
||||
use Mautic\PointBundle\Model\TriggerModel;
|
||||
use Mautic\PointBundle\PointGroupEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class GroupScoreSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private TriggerModel $triggerModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
PointGroupEvents::SCORE_CHANGE => ['onGroupScoreChange', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onGroupScoreChange(GroupScoreChangeEvent $event): void
|
||||
{
|
||||
$this->triggerModel->triggerEvents($event->getContact());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\EventListener;
|
||||
|
||||
use Mautic\LeadBundle\Entity\PointsChangeLogRepository;
|
||||
use Mautic\LeadBundle\Event\LeadEvent;
|
||||
use Mautic\LeadBundle\Event\LeadMergeEvent;
|
||||
use Mautic\LeadBundle\Event\LeadTimelineEvent;
|
||||
use Mautic\LeadBundle\Event\PointsChangeEvent;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Mautic\PointBundle\Entity\LeadPointLogRepository;
|
||||
use Mautic\PointBundle\Entity\LeadTriggerLogRepository;
|
||||
use Mautic\PointBundle\Model\TriggerModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class LeadSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private TriggerModel $triggerModel,
|
||||
private TranslatorInterface $translator,
|
||||
private PointsChangeLogRepository $pointsChangeLogRepository,
|
||||
private LeadPointLogRepository $leadPointLogRepository,
|
||||
private LeadTriggerLogRepository $leadTriggerLogRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
LeadEvents::LEAD_POINTS_CHANGE => ['onLeadPointsChange', 0],
|
||||
LeadEvents::TIMELINE_ON_GENERATE => ['onTimelineGenerate', 0],
|
||||
LeadEvents::LEAD_POST_MERGE => ['onLeadMerge', 0],
|
||||
LeadEvents::LEAD_POST_SAVE => ['onLeadSave', -1],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger applicable events for the lead.
|
||||
*/
|
||||
public function onLeadPointsChange(PointsChangeEvent $event): void
|
||||
{
|
||||
$this->triggerModel->triggerEvents($event->getLead());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle point triggers for new leads (including 0 point triggers).
|
||||
*/
|
||||
public function onLeadSave(LeadEvent $event): void
|
||||
{
|
||||
if ($event->isNew()) {
|
||||
$this->triggerModel->triggerEvents($event->getLead());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile events for the lead timeline.
|
||||
*/
|
||||
public function onTimelineGenerate(LeadTimelineEvent $event): void
|
||||
{
|
||||
// Set available event types
|
||||
$eventTypeKey = 'point.gained';
|
||||
$eventTypeName = $this->translator->trans('mautic.point.event.gained');
|
||||
$event->addEventType($eventTypeKey, $eventTypeName);
|
||||
$event->addSerializerGroup('pointList');
|
||||
|
||||
if (!$event->isApplicable($eventTypeKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$logs = $this->pointsChangeLogRepository->getLeadTimelineEvents($event->getLeadId(), $event->getQueryOptions());
|
||||
|
||||
// Add to counter
|
||||
$event->addToCounter($eventTypeKey, $logs);
|
||||
|
||||
if (!$event->isEngagementCount()) {
|
||||
// Add the logs to the event array
|
||||
foreach ($logs['results'] as $log) {
|
||||
$eventLabel = $log['eventName'].' / '.$log['delta'];
|
||||
if (!empty($log['groupName'])) {
|
||||
$eventLabel .= ' ('.$log['groupName'].')';
|
||||
}
|
||||
|
||||
$event->addEvent(
|
||||
[
|
||||
'event' => $eventTypeKey,
|
||||
'eventId' => $eventTypeKey.$log['id'],
|
||||
'eventLabel' => $eventLabel,
|
||||
'eventType' => $eventTypeName,
|
||||
'timestamp' => $log['dateAdded'],
|
||||
'extra' => [
|
||||
'log' => $log,
|
||||
],
|
||||
'icon' => 'ri-calculator-line',
|
||||
'contactId' => $log['lead_id'],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onLeadMerge(LeadMergeEvent $event): void
|
||||
{
|
||||
$this->leadPointLogRepository->updateLead(
|
||||
$event->getLoser()->getId(),
|
||||
$event->getVictor()->getId()
|
||||
);
|
||||
|
||||
$this->leadTriggerLogRepository->updateLead(
|
||||
$event->getLoser()->getId(),
|
||||
$event->getVictor()->getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\PointBundle\Event as Events;
|
||||
use Mautic\PointBundle\PointEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class PointSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private IpLookupHelper $ipLookupHelper,
|
||||
private AuditLogModel $auditLogModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
PointEvents::POINT_POST_SAVE => ['onPointPostSave', 0],
|
||||
PointEvents::POINT_POST_DELETE => ['onPointDelete', 0],
|
||||
PointEvents::TRIGGER_POST_SAVE => ['onTriggerPostSave', 0],
|
||||
PointEvents::TRIGGER_POST_DELETE => ['onTriggerDelete', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entry to the audit log.
|
||||
*/
|
||||
public function onPointPostSave(Events\PointEvent $event): void
|
||||
{
|
||||
$point = $event->getPoint();
|
||||
if ($details = $event->getChanges()) {
|
||||
$log = [
|
||||
'bundle' => 'point',
|
||||
'object' => 'point',
|
||||
'objectId' => $point->getId(),
|
||||
'action' => ($event->isNew()) ? 'create' : 'update',
|
||||
'details' => $details,
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a delete entry to the audit log.
|
||||
*/
|
||||
public function onPointDelete(Events\PointEvent $event): void
|
||||
{
|
||||
$point = $event->getPoint();
|
||||
$log = [
|
||||
'bundle' => 'point',
|
||||
'object' => 'point',
|
||||
'objectId' => $point->deletedId,
|
||||
'action' => 'delete',
|
||||
'details' => ['name' => $point->getName()],
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entry to the audit log.
|
||||
*/
|
||||
public function onTriggerPostSave(Events\TriggerEvent $event): void
|
||||
{
|
||||
$trigger = $event->getTrigger();
|
||||
if ($details = $event->getChanges()) {
|
||||
$log = [
|
||||
'bundle' => 'point',
|
||||
'object' => 'trigger',
|
||||
'objectId' => $trigger->getId(),
|
||||
'action' => ($event->isNew()) ? 'create' : 'update',
|
||||
'details' => $details,
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a delete entry to the audit log.
|
||||
*/
|
||||
public function onTriggerDelete(Events\TriggerEvent $event): void
|
||||
{
|
||||
$trigger = $event->getTrigger();
|
||||
$log = [
|
||||
'bundle' => 'point',
|
||||
'object' => 'trigger',
|
||||
'objectId' => $trigger->deletedId,
|
||||
'action' => 'delete',
|
||||
'details' => ['name' => $trigger->getName()],
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\EventListener;
|
||||
|
||||
use Mautic\LeadBundle\Report\FieldsBuilder;
|
||||
use Mautic\PointBundle\Entity\Group;
|
||||
use Mautic\PointBundle\Entity\GroupContactScore;
|
||||
use Mautic\ReportBundle\Event\ReportBuilderEvent;
|
||||
use Mautic\ReportBundle\Event\ReportGeneratorEvent;
|
||||
use Mautic\ReportBundle\ReportEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class ReportSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public const CONTEXT_GROUP_SCORE = 'group.score';
|
||||
|
||||
public const GROUP_PREFIX = 'pl';
|
||||
|
||||
public const GROUP_SCORE_PREFIX = 'ls';
|
||||
|
||||
public const GROUP_COLUMNS = [
|
||||
self::GROUP_PREFIX.'.id' => [
|
||||
'alias' => 'group_id',
|
||||
'label' => 'mautic.point.report.group_id',
|
||||
'type' => 'int',
|
||||
],
|
||||
self::GROUP_PREFIX.'.name' => [
|
||||
'alias' => 'group_name',
|
||||
'label' => 'mautic.point.report.group_name',
|
||||
'type' => 'string',
|
||||
],
|
||||
self::GROUP_SCORE_PREFIX.'.score' => [
|
||||
'alias' => 'group_score',
|
||||
'label' => 'mautic.point.report.group_score',
|
||||
'type' => 'int',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $reportContexts = [
|
||||
self::CONTEXT_GROUP_SCORE,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private FieldsBuilder $fieldsBuilder,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
ReportEvents::REPORT_ON_BUILD => ['onReportBuilder', -10],
|
||||
ReportEvents::REPORT_ON_GENERATE => ['onReportGenerate', -10],
|
||||
];
|
||||
}
|
||||
|
||||
public function onReportBuilder(ReportBuilderEvent $event): void
|
||||
{
|
||||
if (!$event->checkContext($this->reportContexts)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event->checkContext(self::CONTEXT_GROUP_SCORE)) {
|
||||
$columns = array_merge(
|
||||
self::GROUP_COLUMNS,
|
||||
$event->getLeadColumns()
|
||||
);
|
||||
$filters = array_merge(
|
||||
$columns,
|
||||
$this->fieldsBuilder->getLeadFilter('l.', 's.')
|
||||
);
|
||||
$data = [
|
||||
'display_name' => 'mautic.point.group.report.table',
|
||||
'columns' => $columns,
|
||||
'filters' => $filters,
|
||||
];
|
||||
$event->addTable(self::CONTEXT_GROUP_SCORE, $data, 'contacts');
|
||||
}
|
||||
}
|
||||
|
||||
public function onReportGenerate(ReportGeneratorEvent $event): void
|
||||
{
|
||||
if (!$event->checkContext($this->reportContexts)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$qb = $event->getQueryBuilder();
|
||||
|
||||
if ($event->checkContext(self::CONTEXT_GROUP_SCORE)) {
|
||||
$qb->from(MAUTIC_TABLE_PREFIX.GroupContactScore::TABLE_NAME, self::GROUP_SCORE_PREFIX)
|
||||
->leftJoin(self::GROUP_SCORE_PREFIX, MAUTIC_TABLE_PREFIX.Group::TABLE_NAME, self::GROUP_PREFIX, self::GROUP_SCORE_PREFIX.'.group_id = '.self::GROUP_PREFIX.'.id')
|
||||
->leftJoin(self::GROUP_SCORE_PREFIX, MAUTIC_TABLE_PREFIX.'leads', 'l', self::GROUP_SCORE_PREFIX.'.contact_id = l.id');
|
||||
|
||||
if ($event->hasFilter('s.leadlist_id')) {
|
||||
$qb->join('l', MAUTIC_TABLE_PREFIX.'lead_lists_leads', 's', 's.lead_id = l.id AND s.manually_removed = 0');
|
||||
}
|
||||
}
|
||||
|
||||
$event->setQueryBuilder($qb);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\CoreEvents;
|
||||
use Mautic\CoreBundle\DTO\GlobalSearchFilterDTO;
|
||||
use Mautic\CoreBundle\Event as MauticEvents;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\GlobalSearch;
|
||||
use Mautic\PointBundle\Model\PointGroupModel;
|
||||
use Mautic\PointBundle\Model\PointModel;
|
||||
use Mautic\PointBundle\Model\TriggerModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class SearchSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private PointModel $pointModel,
|
||||
private TriggerModel $pointTriggerModel,
|
||||
private PointGroupModel $pointGroupModel,
|
||||
private CorePermissions $security,
|
||||
private GlobalSearch $globalSearch,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CoreEvents::GLOBAL_SEARCH => [
|
||||
['onGlobalSearchPointActions', 0],
|
||||
['onGlobalSearchPointTriggers', 0],
|
||||
['onGlobalSearchPointGroup', 0],
|
||||
],
|
||||
CoreEvents::BUILD_COMMAND_LIST => ['onBuildCommandList', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onGlobalSearchPointActions(MauticEvents\GlobalSearchEvent $event): void
|
||||
{
|
||||
$filterDTO = new GlobalSearchFilterDTO($event->getSearchString());
|
||||
$results = $this->globalSearch->performSearch(
|
||||
$filterDTO,
|
||||
$this->pointModel,
|
||||
'@MauticPoint/SubscribedEvents/Search/global_point.html.twig'
|
||||
);
|
||||
|
||||
if (!empty($results)) {
|
||||
$event->addResults('mautic.point.actions.header.index', $results);
|
||||
}
|
||||
}
|
||||
|
||||
public function onGlobalSearchPointGroup(MauticEvents\GlobalSearchEvent $event): void
|
||||
{
|
||||
$filterDTO = new GlobalSearchFilterDTO($event->getSearchString());
|
||||
$results = $this->globalSearch->performSearch(
|
||||
$filterDTO,
|
||||
$this->pointGroupModel,
|
||||
'@MauticPoint/SubscribedEvents/Search/global_group.html.twig'
|
||||
);
|
||||
|
||||
if (!empty($results)) {
|
||||
$event->addResults('mautic.point.group.header.index', $results);
|
||||
}
|
||||
}
|
||||
|
||||
public function onGlobalSearchPointTriggers(MauticEvents\GlobalSearchEvent $event): void
|
||||
{
|
||||
$filterDTO = new GlobalSearchFilterDTO($event->getSearchString());
|
||||
$results = $this->globalSearch->performSearch(
|
||||
$filterDTO,
|
||||
$this->pointTriggerModel,
|
||||
'@MauticPoint/SubscribedEvents/Search/global_trigger.html.twig'
|
||||
);
|
||||
|
||||
if (!empty($results)) {
|
||||
$event->addResults('mautic.point.trigger.header.index', $results);
|
||||
}
|
||||
}
|
||||
|
||||
public function onBuildCommandList(MauticEvents\CommandListEvent $event): void
|
||||
{
|
||||
$security = $this->security;
|
||||
if ($security->isGranted('point:points:view')) {
|
||||
$event->addCommands(
|
||||
'mautic.point.actions.header.index',
|
||||
$this->pointModel->getCommandList()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\EventListener;
|
||||
|
||||
use Mautic\LeadBundle\Event\LeadListFiltersChoicesEvent;
|
||||
use Mautic\LeadBundle\Event\SegmentDictionaryGenerationEvent;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Mautic\LeadBundle\Provider\TypeOperatorProviderInterface;
|
||||
use Mautic\LeadBundle\Segment\Query\Filter\ForeignValueFilterQueryBuilder;
|
||||
use Mautic\PointBundle\Entity\Group;
|
||||
use Mautic\PointBundle\Entity\GroupRepository;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class SegmentFilterSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private GroupRepository $groupRepository,
|
||||
private TypeOperatorProviderInterface $typeOperatorProvider,
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
LeadEvents::LIST_FILTERS_CHOICES_ON_GENERATE => [
|
||||
['onGenerateSegmentFiltersAddPointGroups', -10],
|
||||
],
|
||||
LeadEvents::SEGMENT_DICTIONARY_ON_GENERATE => [
|
||||
['onSegmentDictionaryGenerate', 0],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function onGenerateSegmentFiltersAddPointGroups(LeadListFiltersChoicesEvent $event): void
|
||||
{
|
||||
// Only show for segments and not dynamic content addressed by https://github.com/mautic/mautic/pull/9260
|
||||
if (!$event->isForSegmentation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$groups = $this->groupRepository->getEntities();
|
||||
$choices = [];
|
||||
|
||||
/** @var Group $group */
|
||||
foreach ($groups as $group) {
|
||||
$choices['group_points_'.$group->getId()] = [
|
||||
'label' => $this->translator->trans('mautic.lead.lead.event.grouppoints', ['%group%' => $group->getName()]),
|
||||
'properties' => ['type' => 'number'],
|
||||
'operators' => $this->typeOperatorProvider->getOperatorsForFieldType('default'),
|
||||
'object' => 'lead',
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($choices as $alias => $fieldOptions) {
|
||||
$event->addChoice('groups', $alias, $fieldOptions);
|
||||
}
|
||||
}
|
||||
|
||||
public function onSegmentDictionaryGenerate(SegmentDictionaryGenerationEvent $event): void
|
||||
{
|
||||
$groups = $this->groupRepository->getEntities();
|
||||
|
||||
/** @var Group $group */
|
||||
foreach ($groups as $group) {
|
||||
$event->addTranslation('group_points_'.$group->getId(), [
|
||||
'type' => ForeignValueFilterQueryBuilder::getServiceId(),
|
||||
'foreign_table' => 'point_group_contact_score',
|
||||
'foreign_table_field' => 'contact_id',
|
||||
'table' => 'leads',
|
||||
'table_field' => 'id',
|
||||
'field' => 'score',
|
||||
'where' => 'point_group_contact_score.group_id = '.$group->getId(),
|
||||
'null_value' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\EventListener;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\EventListener\CommonStatsSubscriber;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\PointBundle\Entity\LeadPointLog;
|
||||
use Mautic\PointBundle\Entity\LeadTriggerLog;
|
||||
|
||||
class StatsSubscriber extends CommonStatsSubscriber
|
||||
{
|
||||
public function __construct(CorePermissions $security, EntityManager $entityManager)
|
||||
{
|
||||
parent::__construct($security, $entityManager);
|
||||
$this->addContactRestrictedRepositories(
|
||||
[
|
||||
LeadPointLog::class,
|
||||
LeadTriggerLog::class,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Form\Type;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\Form\DataTransformer\IdToEntityModelTransformer;
|
||||
use Mautic\PointBundle\Entity\Group;
|
||||
use Mautic\PointBundle\Entity\GroupRepository;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<GroupListType>
|
||||
*/
|
||||
class GroupListType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $em,
|
||||
private GroupRepository $repo,
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
if (true === $options['return_entity']) {
|
||||
$transformer = new IdToEntityModelTransformer($this->em, Group::class, 'id');
|
||||
$builder->addModelTransformer($transformer);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'choices' => function (Options $options): array {
|
||||
$groups = $this->repo->getEntities();
|
||||
$choices = [];
|
||||
foreach ($groups as $l) {
|
||||
$choices[$l->getName().' ('.$l->getId().')'] = $l->getId();
|
||||
}
|
||||
|
||||
return $choices;
|
||||
},
|
||||
'label' => 'mautic.point.group.form.group',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'multiple' => false,
|
||||
'required' => false,
|
||||
'return_entity' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getParent(): string
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
|
||||
use Mautic\CoreBundle\Form\EventListener\FormExitSubscriber;
|
||||
use Mautic\CoreBundle\Form\Type\FormButtonsType;
|
||||
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
|
||||
use Mautic\PointBundle\Entity\Group;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<GroupType>
|
||||
*/
|
||||
class GroupType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->addEventSubscriber(new CleanFormSubscriber(['description' => 'html']));
|
||||
$builder->addEventSubscriber(new FormExitSubscriber('point.group', $options));
|
||||
|
||||
$builder->add('name', TextType::class, [
|
||||
'label' => 'mautic.core.name',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
]);
|
||||
|
||||
$builder->add(
|
||||
'description',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'mautic.core.description',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control editor'],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$data = false;
|
||||
if (!empty($options['data']) && $options['data'] instanceof Group) {
|
||||
$data = $options['data']->isPublished(false);
|
||||
}
|
||||
$builder->add('isPublished', YesNoButtonGroupType::class, [
|
||||
'label' => 'mautic.core.form.available',
|
||||
'data' => $data,
|
||||
]);
|
||||
|
||||
$builder->add('buttons', FormButtonsType::class);
|
||||
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults(
|
||||
[
|
||||
'data_class' => Group::class,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class PointActionType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @param FormBuilderInterface<array<mixed>|null> $builder
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$masks = [];
|
||||
$formTypeOptions = [
|
||||
'label' => false,
|
||||
];
|
||||
if (!empty($options['formTypeOptions'])) {
|
||||
$formTypeOptions = array_merge($formTypeOptions, $options['formTypeOptions']);
|
||||
}
|
||||
|
||||
if (isset($options['formType'])) {
|
||||
$builder->add('properties', $options['formType'], $formTypeOptions);
|
||||
}
|
||||
|
||||
if (isset($options['settings']['formTypeCleanMasks'])) {
|
||||
$masks['properties'] = $options['settings']['formTypeCleanMasks'];
|
||||
}
|
||||
|
||||
$builder->addEventSubscriber(new CleanFormSubscriber($masks));
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'formType' => null,
|
||||
'formTypeOptions' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'pointaction';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Form\Type;
|
||||
|
||||
use Mautic\CategoryBundle\Form\Type\CategoryListType;
|
||||
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
|
||||
use Mautic\CoreBundle\Form\EventListener\FormExitSubscriber;
|
||||
use Mautic\CoreBundle\Form\Type\FormButtonsType;
|
||||
use Mautic\CoreBundle\Form\Type\PublishDownDateType;
|
||||
use Mautic\CoreBundle\Form\Type\PublishUpDateType;
|
||||
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\PointBundle\Entity\Point;
|
||||
use Mautic\ProjectBundle\Form\Type\ProjectType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<Point>
|
||||
*/
|
||||
class PointType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private CorePermissions $security,
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->addEventSubscriber(new CleanFormSubscriber(['description' => 'html']));
|
||||
$builder->addEventSubscriber(new FormExitSubscriber('point', $options));
|
||||
|
||||
$builder->add(
|
||||
'name',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.core.name',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'description',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'mautic.core.description',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control editor'],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'type',
|
||||
ChoiceType::class,
|
||||
[
|
||||
'choices' => $options['pointActions']['choices'],
|
||||
'placeholder' => '',
|
||||
'label' => 'mautic.point.form.type',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'onchange' => 'Mautic.getPointActionPropertiesForm(this.value);',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'delta',
|
||||
NumberType::class,
|
||||
[
|
||||
'label' => 'mautic.point.action.delta',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.point.action.delta.help',
|
||||
],
|
||||
'scale' => 0,
|
||||
]
|
||||
);
|
||||
|
||||
$type = (!empty($options['actionType'])) ? $options['actionType'] : $options['data']->getType();
|
||||
|
||||
if ($type && !empty($options['pointActions']['actions'][$type]['formType'])) {
|
||||
$formType = $options['pointActions']['actions'][$type]['formType'];
|
||||
$properties = ($options['data']) ? $options['data']->getProperties() : [];
|
||||
$builder->add(
|
||||
'properties',
|
||||
$formType,
|
||||
[
|
||||
'label' => false,
|
||||
'data' => $properties,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$builder->add(
|
||||
'group',
|
||||
GroupListType::class,
|
||||
[
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.point.group.form.group_descr',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
if (!empty($options['data']) && $options['data'] instanceof Point) {
|
||||
$readonly = !$this->security->hasEntityAccess(
|
||||
'point:points:publishown',
|
||||
'point:points:publishother',
|
||||
$options['data']->getCreatedBy()
|
||||
);
|
||||
|
||||
$data = $options['data']->isPublished(false);
|
||||
} elseif (!$this->security->isGranted('point:points:publishown')) {
|
||||
$readonly = true;
|
||||
$data = false;
|
||||
} else {
|
||||
$readonly = false;
|
||||
$data = true;
|
||||
}
|
||||
|
||||
$builder->add(
|
||||
'isPublished',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'data' => $data,
|
||||
'attr' => [
|
||||
'readonly' => $readonly,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'repeatable',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.point.form.repeat',
|
||||
'data' => $options['data']->getRepeatable() ?: false,
|
||||
'attr' => [
|
||||
'tooltip' => 'mautic.point.form.repeat.help',
|
||||
],
|
||||
'yes_label' => 'mautic.point.form.repeat.yes',
|
||||
'no_label' => 'mautic.point.form.repeat.no',
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('publishUp', PublishUpDateType::class);
|
||||
$builder->add('publishDown', PublishDownDateType::class);
|
||||
|
||||
$builder->add(
|
||||
'category',
|
||||
CategoryListType::class,
|
||||
[
|
||||
'bundle' => 'point',
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('projects', ProjectType::class);
|
||||
|
||||
$builder->add('buttons', FormButtonsType::class);
|
||||
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults(['data_class' => Point::class]);
|
||||
$resolver->setRequired(['pointActions']);
|
||||
$resolver->setDefined(['actionType']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
|
||||
use Mautic\CoreBundle\Form\Type\FormButtonsType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class TriggerEventType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @param FormBuilderInterface<array<mixed>|null> $builder
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$masks = ['description' => 'html'];
|
||||
|
||||
$builder->add(
|
||||
'name',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.core.name',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'description',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'mautic.core.description',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control editor'],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
if (!empty($options['settings']['formType'])) {
|
||||
$properties = (!empty($options['data']['properties'])) ? $options['data']['properties'] : null;
|
||||
|
||||
$formTypeOptions = [
|
||||
'label' => false,
|
||||
'data' => $properties,
|
||||
];
|
||||
if (!empty($options['settings']['formTypeOptions'])) {
|
||||
$formTypeOptions = array_merge($formTypeOptions, $options['settings']['formTypeOptions']);
|
||||
}
|
||||
|
||||
if (isset($options['settings']['formTypeCleanMasks'])) {
|
||||
$masks['properties'] = $options['settings']['formTypeCleanMasks'];
|
||||
}
|
||||
|
||||
$builder->add('properties', $options['settings']['formType'], $formTypeOptions);
|
||||
}
|
||||
|
||||
$builder->add('type', HiddenType::class);
|
||||
|
||||
$update = !empty($properties);
|
||||
if (!empty($update)) {
|
||||
$btnValue = 'mautic.core.form.update';
|
||||
$btnIcon = 'ri-edit-line';
|
||||
} else {
|
||||
$btnValue = 'mautic.core.form.add';
|
||||
$btnIcon = 'ri-add-line';
|
||||
}
|
||||
|
||||
$builder->add(
|
||||
'buttons',
|
||||
FormButtonsType::class,
|
||||
[
|
||||
'save_text' => $btnValue,
|
||||
'save_icon' => $btnIcon,
|
||||
'apply_text' => false,
|
||||
'container_class' => 'bottom-form-buttons',
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'triggerId',
|
||||
HiddenType::class,
|
||||
[
|
||||
'mapped' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->addEventSubscriber(new CleanFormSubscriber($masks));
|
||||
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults(['settings' => false]);
|
||||
$resolver->setRequired(['settings']);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'pointtriggerevent';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Form\Type;
|
||||
|
||||
use Mautic\CategoryBundle\Form\Type\CategoryListType;
|
||||
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
|
||||
use Mautic\CoreBundle\Form\EventListener\FormExitSubscriber;
|
||||
use Mautic\CoreBundle\Form\Type\FormButtonsType;
|
||||
use Mautic\CoreBundle\Form\Type\PublishDownDateType;
|
||||
use Mautic\CoreBundle\Form\Type\PublishUpDateType;
|
||||
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\PointBundle\Entity\Trigger;
|
||||
use Mautic\ProjectBundle\Form\Type\ProjectType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<Trigger>
|
||||
*/
|
||||
class TriggerType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private CorePermissions $security,
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->addEventSubscriber(new CleanFormSubscriber(['description' => 'html']));
|
||||
$builder->addEventSubscriber(new FormExitSubscriber('point', $options));
|
||||
|
||||
$builder->add(
|
||||
'name',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.core.name',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'description',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'mautic.core.description',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control editor'],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'category',
|
||||
CategoryListType::class,
|
||||
[
|
||||
'bundle' => 'point',
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('projects', ProjectType::class);
|
||||
|
||||
$builder->add(
|
||||
'points',
|
||||
NumberType::class,
|
||||
[
|
||||
'label' => 'mautic.point.trigger.form.points',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.point.trigger.form.points_descr',
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$color = $options['data']->getColor();
|
||||
|
||||
$builder->add(
|
||||
'color',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.point.trigger.form.color',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'data-toggle' => 'color',
|
||||
'tooltip' => 'mautic.point.trigger.form.color_descr',
|
||||
],
|
||||
'required' => false,
|
||||
'data' => (!empty($color)) ? $color : 'a0acb8',
|
||||
'empty_data' => 'a0acb8',
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'group',
|
||||
GroupListType::class,
|
||||
[
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.point.group.form.group_descr',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'triggerExistingLeads',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.point.trigger.form.existingleads',
|
||||
]
|
||||
);
|
||||
|
||||
if (!empty($options['data']) && $options['data']->getId()) {
|
||||
$readonly = !$this->security->isGranted('point:triggers:publish');
|
||||
$data = $options['data']->isPublished(false);
|
||||
} elseif (!$this->security->isGranted('point:triggers:publish')) {
|
||||
$readonly = true;
|
||||
$data = false;
|
||||
} else {
|
||||
$readonly = false;
|
||||
$data = false;
|
||||
}
|
||||
|
||||
$builder->add(
|
||||
'isPublished',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'data' => $data,
|
||||
'attr' => [
|
||||
'readonly' => $readonly,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('publishUp', PublishUpDateType::class);
|
||||
$builder->add('publishDown', PublishDownDateType::class);
|
||||
|
||||
$builder->add(
|
||||
'sessionId',
|
||||
HiddenType::class,
|
||||
[
|
||||
'mapped' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('buttons', FormButtonsType::class);
|
||||
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults(
|
||||
[
|
||||
'data_class' => Trigger::class,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'pointtrigger';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Helper;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
|
||||
class EventHelper
|
||||
{
|
||||
/**
|
||||
* @param Lead $lead
|
||||
* @param array $action
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function engagePointAction($lead, $action)
|
||||
{
|
||||
static $initiated = [];
|
||||
|
||||
$pointsChange = 0;
|
||||
|
||||
// only initiate once per lead per type
|
||||
if (empty($initiated[$lead->getId()][$action['type']])) {
|
||||
if (!empty($action['points'])) {
|
||||
$pointsChange = $action['points'];
|
||||
$initiated[$lead->getId()][$action['type']] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $pointsChange;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class MauticPointBundle extends Bundle
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Model;
|
||||
|
||||
use Mautic\CoreBundle\Model\FormModel as CommonFormModel;
|
||||
use Mautic\CoreBundle\Model\GlobalSearchInterface;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\PointBundle\Entity\Group;
|
||||
use Mautic\PointBundle\Entity\GroupContactScore;
|
||||
use Mautic\PointBundle\Entity\GroupRepository;
|
||||
use Mautic\PointBundle\Event as Events;
|
||||
use Mautic\PointBundle\Form\Type\GroupType;
|
||||
use Mautic\PointBundle\PointGroupEvents;
|
||||
use Symfony\Component\Form\FormFactory;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* @extends CommonFormModel<Group>
|
||||
*/
|
||||
class PointGroupModel extends CommonFormModel implements GlobalSearchInterface
|
||||
{
|
||||
public function getRepository(): GroupRepository
|
||||
{
|
||||
return $this->em->getRepository(Group::class);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'point:groups';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $entity
|
||||
* @param FormFactory $formFactory
|
||||
* @param string|null $action
|
||||
* @param array<string,string> $options
|
||||
*
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
public function createForm($entity, $formFactory, $action = null, $options = []): FormInterface
|
||||
{
|
||||
if (!$entity instanceof Group) {
|
||||
throw new MethodNotAllowedHttpException(['Group']);
|
||||
}
|
||||
|
||||
if (!empty($action)) {
|
||||
$options['action'] = $action;
|
||||
}
|
||||
|
||||
return $formFactory->create(GroupType::class, $entity, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific entity or generate a new one if id is empty.
|
||||
*
|
||||
* @param int $id
|
||||
*/
|
||||
public function getEntity($id = null): ?Group
|
||||
{
|
||||
if (null === $id) {
|
||||
return new Group();
|
||||
}
|
||||
|
||||
return parent::getEntity($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
|
||||
{
|
||||
if (!$entity instanceof Group) {
|
||||
throw new MethodNotAllowedHttpException(['Group']);
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'pre_save':
|
||||
$name = PointGroupEvents::GROUP_PRE_SAVE;
|
||||
break;
|
||||
case 'post_save':
|
||||
$name = PointGroupEvents::GROUP_POST_SAVE;
|
||||
break;
|
||||
case 'pre_delete':
|
||||
$name = PointGroupEvents::GROUP_PRE_DELETE;
|
||||
break;
|
||||
case 'post_delete':
|
||||
$name = PointGroupEvents::GROUP_POST_DELETE;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->dispatcher->hasListeners($name)) {
|
||||
if (empty($event)) {
|
||||
$event = new Events\GroupEvent($entity);
|
||||
}
|
||||
$this->dispatcher->dispatch($event, $name);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function adjustPoints(Lead $contact, Group $group, int $points, string $operator = Lead::POINTS_ADD): Lead
|
||||
{
|
||||
$contactScore = $contact->getGroupScore($group);
|
||||
|
||||
if (empty($contactScore)) {
|
||||
$contactScore = new GroupContactScore();
|
||||
$contactScore->setContact($contact);
|
||||
$contactScore->setGroup($group);
|
||||
$contactScore->setScore(0);
|
||||
$contact->addGroupScore($contactScore);
|
||||
}
|
||||
$oldScore = $contactScore->getScore();
|
||||
$newScore = $oldScore;
|
||||
|
||||
match ($operator) {
|
||||
Lead::POINTS_ADD => $newScore += $points,
|
||||
Lead::POINTS_SUBTRACT => $newScore -= $points,
|
||||
Lead::POINTS_MULTIPLY => $newScore *= $points,
|
||||
Lead::POINTS_DIVIDE => $newScore /= $points,
|
||||
Lead::POINTS_SET => $newScore = $points,
|
||||
default => throw new \UnexpectedValueException('Invalid operator'),
|
||||
};
|
||||
$contactScore->setScore($newScore);
|
||||
$this->em->persist($contactScore);
|
||||
$this->em->flush();
|
||||
|
||||
$scoreChangeEvent = new Events\GroupScoreChangeEvent($contactScore, $oldScore, $newScore);
|
||||
$this->dispatcher->dispatch($scoreChangeEvent, PointGroupEvents::SCORE_CHANGE);
|
||||
|
||||
return $contact;
|
||||
}
|
||||
|
||||
public static function isAllowedPointOperation(string $operator): bool
|
||||
{
|
||||
return in_array($operator, [Lead::POINTS_ADD, Lead::POINTS_SUBTRACT, Lead::POINTS_MULTIPLY, Lead::POINTS_DIVIDE, Lead::POINTS_SET]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
use Mautic\CoreBundle\Helper\Chart\LineChart;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Model\FormModel as CommonFormModel;
|
||||
use Mautic\CoreBundle\Model\GlobalSearchInterface;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\PointBundle\Entity\LeadPointLog;
|
||||
use Mautic\PointBundle\Entity\Point;
|
||||
use Mautic\PointBundle\Entity\PointRepository;
|
||||
use Mautic\PointBundle\Event\PointActionEvent;
|
||||
use Mautic\PointBundle\Event\PointBuilderEvent;
|
||||
use Mautic\PointBundle\Event\PointEvent;
|
||||
use Mautic\PointBundle\Form\Type\PointType;
|
||||
use Mautic\PointBundle\PointEvents;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonFormModel<Point>
|
||||
*/
|
||||
class PointModel extends CommonFormModel implements GlobalSearchInterface, ResetInterface
|
||||
{
|
||||
/**
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private array $actions = [];
|
||||
|
||||
public function __construct(
|
||||
protected RequestStack $requestStack,
|
||||
protected IpLookupHelper $ipLookupHelper,
|
||||
protected LeadModel $leadModel,
|
||||
private ContactTracker $contactTracker,
|
||||
EntityManager $em,
|
||||
CorePermissions $security,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
UrlGeneratorInterface $router,
|
||||
Translator $translator,
|
||||
UserHelper $userHelper,
|
||||
LoggerInterface $mauticLogger,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
private PointGroupModel $pointGroupModel,
|
||||
) {
|
||||
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PointRepository
|
||||
*/
|
||||
public function getRepository()
|
||||
{
|
||||
return $this->em->getRepository(Point::class);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'point:points';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface
|
||||
{
|
||||
if (!$entity instanceof Point) {
|
||||
throw new MethodNotAllowedHttpException(['Point']);
|
||||
}
|
||||
|
||||
if (!empty($action)) {
|
||||
$options['action'] = $action;
|
||||
}
|
||||
|
||||
if (empty($options['pointActions'])) {
|
||||
$options['pointActions'] = $this->getPointActions();
|
||||
}
|
||||
|
||||
return $formFactory->create(PointType::class, $entity, $options);
|
||||
}
|
||||
|
||||
public function getEntity($id = null): ?Point
|
||||
{
|
||||
if (null === $id) {
|
||||
return new Point();
|
||||
}
|
||||
|
||||
return parent::getEntity($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
|
||||
{
|
||||
if (!$entity instanceof Point) {
|
||||
throw new MethodNotAllowedHttpException(['Point']);
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'pre_save':
|
||||
$name = PointEvents::POINT_PRE_SAVE;
|
||||
break;
|
||||
case 'post_save':
|
||||
$name = PointEvents::POINT_POST_SAVE;
|
||||
break;
|
||||
case 'pre_delete':
|
||||
$name = PointEvents::POINT_PRE_DELETE;
|
||||
break;
|
||||
case 'post_delete':
|
||||
$name = PointEvents::POINT_POST_DELETE;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->dispatcher->hasListeners($name)) {
|
||||
if (empty($event)) {
|
||||
$event = new PointEvent($entity, $isNew);
|
||||
$event->setEntityManager($this->em);
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch($event, $name);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets array of custom actions from bundles subscribed PointEvents::POINT_ON_BUILD.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPointActions()
|
||||
{
|
||||
if ([] === $this->actions) {
|
||||
// build them
|
||||
$this->actions = [];
|
||||
$event = new PointBuilderEvent($this->translator);
|
||||
$this->dispatcher->dispatch($event, PointEvents::POINT_ON_BUILD);
|
||||
$this->actions['actions'] = $event->getActions();
|
||||
$this->actions['list'] = $event->getActionList();
|
||||
$this->actions['choices'] = $event->getActionChoices();
|
||||
}
|
||||
|
||||
return $this->actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a specific point change.
|
||||
*
|
||||
* @param mixed $eventDetails passthrough from function triggering action to the callback function
|
||||
* @param mixed $typeId Something unique to the triggering event to prevent unnecessary duplicate calls
|
||||
* @param bool $allowUserRequest
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public function triggerAction($type, $eventDetails = null, $typeId = null, ?Lead $lead = null, $allowUserRequest = false): void
|
||||
{
|
||||
// only trigger actions for not logged Mautic users
|
||||
if (!$this->security->isAnonymous() && !$allowUserRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $typeId && MAUTIC_ENV === 'prod' && null !== $this->requestStack->getMainRequest()) {
|
||||
// let's prevent some unnecessary DB calls
|
||||
$session = $this->requestStack->getMainRequest()->getSession();
|
||||
$triggeredEvents = $session->get('mautic.triggered.point.actions', []);
|
||||
if (in_array($typeId, $triggeredEvents)) {
|
||||
return;
|
||||
}
|
||||
$triggeredEvents[] = $typeId;
|
||||
$session->set('mautic.triggered.point.actions', $triggeredEvents);
|
||||
}
|
||||
|
||||
// find all the actions for published points
|
||||
/** @var PointRepository $repo */
|
||||
$repo = $this->getRepository();
|
||||
$availablePoints = $repo->getPublishedByType($type);
|
||||
if (empty($availablePoints)) {
|
||||
return;
|
||||
}
|
||||
$ipAddress = $this->ipLookupHelper->getIpAddress();
|
||||
|
||||
$hasLeadPointChanges = false;
|
||||
if (null === $lead) {
|
||||
$lead = $this->contactTracker->getContact();
|
||||
|
||||
if (null === $lead || !$lead->getId()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// get available actions
|
||||
$availableActions = $this->getPointActions();
|
||||
|
||||
// get a list of actions that has already been performed on this lead
|
||||
$completedActions = $repo->getCompletedLeadActions($type, $lead->getId());
|
||||
|
||||
$persist = [];
|
||||
/** @var Point $action */
|
||||
foreach ($availablePoints as $action) {
|
||||
// if it's already been done or not repeatable, then skip it
|
||||
if (!$action->getRepeatable() && isset($completedActions[$action->getId()])) {
|
||||
continue;
|
||||
}
|
||||
// make sure the action still exists
|
||||
if (!isset($availableActions['actions'][$action->getType()])) {
|
||||
continue;
|
||||
}
|
||||
$settings = $availableActions['actions'][$action->getType()];
|
||||
|
||||
$args = [
|
||||
'action' => [
|
||||
'id' => $action->getId(),
|
||||
'type' => $action->getType(),
|
||||
'name' => $action->getName(),
|
||||
'properties' => $action->getProperties(),
|
||||
'points' => $action->getDelta(),
|
||||
],
|
||||
'lead' => $lead,
|
||||
'eventDetails' => $eventDetails,
|
||||
];
|
||||
|
||||
$callback = $settings['callback'] ?? [\Mautic\PointBundle\Helper\EventHelper::class, 'engagePointAction'];
|
||||
|
||||
if (is_callable($callback)) {
|
||||
$object = null;
|
||||
if (is_array($callback)) {
|
||||
$reflection = new \ReflectionMethod($callback[0], $callback[1]);
|
||||
if (is_object($callback[0])) {
|
||||
$object = $callback[0];
|
||||
}
|
||||
} elseif (str_contains($callback, '::')) {
|
||||
$parts = explode('::', $callback);
|
||||
$reflection = new \ReflectionMethod($parts[0], $parts[1]);
|
||||
} else {
|
||||
$reflection = new \ReflectionMethod(null, $callback);
|
||||
}
|
||||
|
||||
$pass = [];
|
||||
foreach ($reflection->getParameters() as $param) {
|
||||
if (isset($args[$param->getName()])) {
|
||||
$pass[] = $args[$param->getName()];
|
||||
} else {
|
||||
$pass[] = null;
|
||||
}
|
||||
}
|
||||
|
||||
$pointsChange = $reflection->invokeArgs($object, $pass);
|
||||
|
||||
if ($pointsChange) {
|
||||
$delta = $action->getDelta();
|
||||
|
||||
$pointsChangeLogEntryName = $action->getId().': '.$action->getName();
|
||||
$pointGroup = $action->getGroup();
|
||||
if (!empty($pointGroup)) {
|
||||
$this->pointGroupModel->adjustPoints($lead, $pointGroup, $delta);
|
||||
} else {
|
||||
$lead->adjustPoints($delta);
|
||||
}
|
||||
|
||||
$hasLeadPointChanges = true;
|
||||
$parsed = explode('.', $action->getType());
|
||||
$lead->addPointsChangeLogEntry(
|
||||
$parsed[0],
|
||||
$pointsChangeLogEntryName,
|
||||
$parsed[1],
|
||||
$delta,
|
||||
$ipAddress,
|
||||
$pointGroup
|
||||
);
|
||||
|
||||
$event = new PointActionEvent($action, $lead);
|
||||
$this->dispatcher->dispatch($event, PointEvents::POINT_ON_ACTION);
|
||||
|
||||
if (!$action->getRepeatable()) {
|
||||
$log = new LeadPointLog();
|
||||
$log->setIpAddress($ipAddress);
|
||||
$log->setPoint($action);
|
||||
$log->setLead($lead);
|
||||
$log->setDateFired(new \DateTime());
|
||||
$persist[] = $log;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($persist)) {
|
||||
$this->getRepository()->saveEntities($persist);
|
||||
$this->getRepository()->detachEntities($persist);
|
||||
}
|
||||
|
||||
if ($hasLeadPointChanges) {
|
||||
$this->leadModel->saveEntity($lead);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get line chart data of points.
|
||||
*
|
||||
* @param string $unit {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters}
|
||||
* @param string $dateFormat
|
||||
* @param array $filter
|
||||
* @param bool $canViewOthers
|
||||
*/
|
||||
public function getPointLineChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true): array
|
||||
{
|
||||
$chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat);
|
||||
$query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
|
||||
$q = $query->prepareTimeDataQuery('lead_points_change_log', 'date_added', $filter);
|
||||
|
||||
if (!$canViewOthers) {
|
||||
$q->join('t', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = t.lead_id')
|
||||
->andWhere('l.owner_id = :userId')
|
||||
->setParameter('userId', $this->userHelper->getUser()->getId());
|
||||
}
|
||||
|
||||
$data = $query->loadAndBuildTimeData($q);
|
||||
$chart->setDataset($this->translator->trans('mautic.point.changes'), $data);
|
||||
|
||||
return $chart->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getPointActionIdsWithDependenciesOnEmail(int $emailId): array
|
||||
{
|
||||
$filter = [
|
||||
'force' => [
|
||||
['column' => 'p.type', 'expr' => 'in', 'value' => ['email.send', 'email.open']],
|
||||
],
|
||||
];
|
||||
$entities = $this->getEntities(
|
||||
[
|
||||
'filter' => $filter,
|
||||
]
|
||||
);
|
||||
$pointActionIds = [];
|
||||
foreach ($entities as $entity) {
|
||||
$properties = $entity->getProperties();
|
||||
if (in_array($emailId, $properties['emails'] ?? [])) {
|
||||
$pointActionIds[] = $entity->getId();
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($pointActionIds);
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->actions = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Model;
|
||||
|
||||
use Mautic\CoreBundle\Model\FormModel as CommonFormModel;
|
||||
use Mautic\PointBundle\Entity\TriggerEvent;
|
||||
use Mautic\PointBundle\Entity\TriggerEventRepository;
|
||||
use Mautic\PointBundle\Form\Type\TriggerEventType;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
|
||||
/**
|
||||
* @extends CommonFormModel<TriggerEvent>
|
||||
*/
|
||||
class TriggerEventModel extends CommonFormModel
|
||||
{
|
||||
/**
|
||||
* @return TriggerEventRepository
|
||||
*/
|
||||
public function getRepository()
|
||||
{
|
||||
return $this->em->getRepository(TriggerEvent::class);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'point:triggers';
|
||||
}
|
||||
|
||||
public function getEntity($id = null): ?TriggerEvent
|
||||
{
|
||||
if (null === $id) {
|
||||
return new TriggerEvent();
|
||||
}
|
||||
|
||||
return parent::getEntity($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface
|
||||
{
|
||||
if (!$entity instanceof TriggerEvent) {
|
||||
throw new MethodNotAllowedHttpException(['Trigger']);
|
||||
}
|
||||
|
||||
if (!empty($action)) {
|
||||
$options['action'] = $action;
|
||||
}
|
||||
|
||||
return $formFactory->create(TriggerEventType::class, $entity, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get segments which are dependent on given segment.
|
||||
*
|
||||
* @param int $segmentId
|
||||
*/
|
||||
public function getReportIdsWithDependenciesOnSegment($segmentId): array
|
||||
{
|
||||
$filter = [
|
||||
'force' => [
|
||||
['column' => 'e.type', 'expr' => 'eq', 'value'=>'lead.changelists'],
|
||||
],
|
||||
];
|
||||
$entities = $this->getEntities(
|
||||
[
|
||||
'filter' => $filter,
|
||||
]
|
||||
);
|
||||
$dependents = [];
|
||||
foreach ($entities as $entity) {
|
||||
$retrFilters = $entity->getProperties();
|
||||
foreach ($retrFilters as $eachFilter) {
|
||||
if (in_array($segmentId, $eachFilter)) {
|
||||
$dependents[] = $entity->getTrigger()->getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $dependents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getPointTriggerIdsWithDependenciesOnEmail(int $emailId): array
|
||||
{
|
||||
$filter = [
|
||||
'force' => [
|
||||
['column' => 'e.type', 'expr' => 'in', 'value' => ['email.send', 'email.send_to_user']],
|
||||
],
|
||||
];
|
||||
$entities = $this->getEntities(
|
||||
[
|
||||
'filter' => $filter,
|
||||
]
|
||||
);
|
||||
$triggerIds = [];
|
||||
foreach ($entities as $entity) {
|
||||
$properties = $entity->getProperties();
|
||||
if (isset($properties['email']) && (int) $properties['email'] === $emailId) {
|
||||
$triggerIds[] = $entity->getTrigger()->getId();
|
||||
}
|
||||
if (isset($properties['useremail']['email']) && (int) $properties['useremail']['email'] === $emailId) {
|
||||
$triggerIds[] = $entity->getTrigger()->getId();
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($triggerIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getPointTriggerIdsWithDependenciesOnTag(string $tagName): array
|
||||
{
|
||||
$filter = [
|
||||
'force' => [
|
||||
['column' => 'e.type', 'expr' => 'eq', 'value' => 'lead.changetags'],
|
||||
],
|
||||
];
|
||||
$entities = $this->getEntities(
|
||||
[
|
||||
'filter' => $filter,
|
||||
]
|
||||
);
|
||||
$triggerIds = [];
|
||||
foreach ($entities as $entity) {
|
||||
$properties = $entity->getProperties();
|
||||
foreach ($properties as $property) {
|
||||
if (in_array($tagName, $property)) {
|
||||
$triggerIds[] = $entity->getTrigger()->getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($triggerIds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Model\FormModel as CommonFormModel;
|
||||
use Mautic\CoreBundle\Model\GlobalSearchInterface;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\PointBundle\Entity\GroupContactScore;
|
||||
use Mautic\PointBundle\Entity\LeadTriggerLog;
|
||||
use Mautic\PointBundle\Entity\Trigger;
|
||||
use Mautic\PointBundle\Entity\TriggerEvent;
|
||||
use Mautic\PointBundle\Event as Events;
|
||||
use Mautic\PointBundle\Event\TriggerBuilderEvent;
|
||||
use Mautic\PointBundle\Form\Type\TriggerType;
|
||||
use Mautic\PointBundle\PointEvents;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* @extends CommonFormModel<Trigger>
|
||||
*/
|
||||
class TriggerModel extends CommonFormModel implements GlobalSearchInterface
|
||||
{
|
||||
protected $triggers = [];
|
||||
|
||||
/**
|
||||
* @var array<string, mixed[]>
|
||||
*/
|
||||
private $cachedEvents = [];
|
||||
|
||||
public function __construct(
|
||||
protected IpLookupHelper $ipLookupHelper,
|
||||
protected LeadModel $leadModel,
|
||||
protected TriggerEventModel $pointTriggerEventModel,
|
||||
private ContactTracker $contactTracker,
|
||||
EntityManagerInterface $em,
|
||||
CorePermissions $security,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
UrlGeneratorInterface $router,
|
||||
Translator $translator,
|
||||
UserHelper $userHelper,
|
||||
LoggerInterface $mauticLogger,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Mautic\PointBundle\Entity\TriggerRepository
|
||||
*/
|
||||
public function getRepository()
|
||||
{
|
||||
return $this->em->getRepository(Trigger::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an instance of the TriggerEventRepository.
|
||||
*
|
||||
* @return \Mautic\PointBundle\Entity\TriggerEventRepository
|
||||
*/
|
||||
public function getEventRepository()
|
||||
{
|
||||
return $this->em->getRepository(TriggerEvent::class);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'point:triggers';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface
|
||||
{
|
||||
if (!$entity instanceof Trigger) {
|
||||
throw new MethodNotAllowedHttpException(['Trigger']);
|
||||
}
|
||||
|
||||
if (!empty($action)) {
|
||||
$options['action'] = $action;
|
||||
}
|
||||
|
||||
return $formFactory->create(TriggerType::class, $entity, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Trigger $entity
|
||||
* @param bool $unlock
|
||||
*/
|
||||
public function saveEntity($entity, $unlock = true): void
|
||||
{
|
||||
$isNew = ($entity->getId()) ? false : true;
|
||||
|
||||
parent::saveEntity($entity, $unlock);
|
||||
|
||||
// should we trigger for existing leads?
|
||||
if ($entity->getTriggerExistingLeads() && $entity->isPublished()) {
|
||||
$events = $entity->getEvents();
|
||||
$repo = $this->getEventRepository();
|
||||
$persist = [];
|
||||
$ipAddress = $this->ipLookupHelper->getIpAddress();
|
||||
$pointGroup = $entity->getGroup();
|
||||
|
||||
/** @var LeadRepository $leadRepository */
|
||||
$leadRepository = $this->em->getRepository(Lead::class);
|
||||
|
||||
foreach ($events as $event) {
|
||||
$args = [
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'l.date_added',
|
||||
'expr' => 'lte',
|
||||
'value' => (new DateTimeHelper($entity->getDateAdded()))->toUtcString(),
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (!$pointGroup) {
|
||||
$args['filter']['force'][] = [
|
||||
'column' => 'l.points',
|
||||
'expr' => 'gte',
|
||||
'value' => $entity->getPoints(),
|
||||
];
|
||||
} else {
|
||||
$args['qb'] = $leadRepository->getEntitiesDbalQueryBuilder()
|
||||
->leftJoin('l', MAUTIC_TABLE_PREFIX.GroupContactScore::TABLE_NAME, 'pls', 'l.id = pls.contact_id');
|
||||
$args['filter']['force'][] = [
|
||||
'column' => 'pls.score',
|
||||
'expr' => 'gte',
|
||||
'value' => $entity->getPoints(),
|
||||
];
|
||||
$args['filter']['force'][] = [
|
||||
'column' => 'pls.group_id',
|
||||
'expr' => 'eq',
|
||||
'value' => $entity->getGroup()->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
if (!$isNew) {
|
||||
// get a list of leads that has already had this event applied
|
||||
$leadIds = $repo->getLeadsForEvent($event->getId());
|
||||
if (!empty($leadIds)) {
|
||||
$args['filter']['force'][] = [
|
||||
'column' => 'l.id',
|
||||
'expr' => 'notIn',
|
||||
'value' => $leadIds,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// get a list of leads that are before the trigger's date_added and trigger if not already done so
|
||||
$leads = $this->leadModel->getEntities($args);
|
||||
|
||||
/** @var Lead $l */
|
||||
foreach ($leads as $l) {
|
||||
if ($this->triggerEvent($event->convertToArray(), $l, true)) {
|
||||
$log = new LeadTriggerLog();
|
||||
$log->setIpAddress($ipAddress);
|
||||
$log->setEvent($event);
|
||||
$log->setLead($l);
|
||||
$log->setDateFired(new \DateTime());
|
||||
$event->addLog($log);
|
||||
$persist[] = $event;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($persist)) {
|
||||
$repo->saveEntities($persist);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getEntity($id = null): ?Trigger
|
||||
{
|
||||
if (null === $id) {
|
||||
return new Trigger();
|
||||
}
|
||||
|
||||
return parent::getEntity($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
|
||||
{
|
||||
if (!$entity instanceof Trigger) {
|
||||
throw new MethodNotAllowedHttpException(['Trigger']);
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'pre_save':
|
||||
$name = PointEvents::TRIGGER_PRE_SAVE;
|
||||
break;
|
||||
case 'post_save':
|
||||
$name = PointEvents::TRIGGER_POST_SAVE;
|
||||
break;
|
||||
case 'pre_delete':
|
||||
$name = PointEvents::TRIGGER_PRE_DELETE;
|
||||
break;
|
||||
case 'post_delete':
|
||||
$name = PointEvents::TRIGGER_POST_DELETE;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->dispatcher->hasListeners($name)) {
|
||||
if (empty($event)) {
|
||||
$event = new Events\TriggerEvent($entity, $isNew);
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch($event, $name);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sessionEvents
|
||||
*/
|
||||
public function setEvents(Trigger $entity, $sessionEvents): void
|
||||
{
|
||||
$order = 1;
|
||||
$existingActions = $entity->getEvents();
|
||||
|
||||
foreach ($sessionEvents as $properties) {
|
||||
$isNew = (!empty($properties['id']) && isset($existingActions[$properties['id']])) ? false : true;
|
||||
$event = !$isNew ? $existingActions[$properties['id']] : new TriggerEvent();
|
||||
|
||||
foreach ($properties as $f => $v) {
|
||||
if (in_array($f, ['id', 'order'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$func = 'set'.ucfirst($f);
|
||||
if (method_exists($event, $func)) {
|
||||
$event->$func($v);
|
||||
}
|
||||
}
|
||||
$event->setTrigger($entity);
|
||||
$event->setOrder($order);
|
||||
++$order;
|
||||
$entity->addTriggerEvent($properties['id'], $event);
|
||||
}
|
||||
|
||||
// Persist if editing the trigger
|
||||
if ($entity->getId()) {
|
||||
$this->pointTriggerEventModel->saveEntities($entity->getEvents());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets array of custom events from bundles subscribed PointEvents::TRIGGER_ON_BUILD.
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getEvents()
|
||||
{
|
||||
if (empty($this->cachedEvents)) {
|
||||
$event = new TriggerBuilderEvent($this->translator);
|
||||
$this->dispatcher->dispatch($event, PointEvents::TRIGGER_ON_BUILD);
|
||||
$this->cachedEvents = $event->getEvents();
|
||||
}
|
||||
|
||||
return $this->cachedEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets array of custom events from bundles inside groups.
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getEventGroups(): array
|
||||
{
|
||||
$events = $this->getEvents();
|
||||
$groups = [];
|
||||
foreach ($events as $key => $event) {
|
||||
$groups[$event['group']][$key] = $event;
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a specific event.
|
||||
*
|
||||
* @param array $event triggerEvent converted to array
|
||||
* @param bool $force
|
||||
*
|
||||
* @return bool Was event triggered
|
||||
*/
|
||||
public function triggerEvent($event, ?Lead $lead = null, $force = false)
|
||||
{
|
||||
// only trigger events for anonymous users
|
||||
if (!$force && !$this->security->isAnonymous()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null === $lead) {
|
||||
$lead = $this->contactTracker->getContact();
|
||||
}
|
||||
|
||||
if (!$force) {
|
||||
// get a list of events that has already been performed on this lead
|
||||
$appliedEvents = $this->getEventRepository()->getLeadTriggeredEvents($lead->getId());
|
||||
|
||||
// if it's already been done, then skip it
|
||||
if (isset($appliedEvents[$event['id']])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$availableEvents = $this->getEvents();
|
||||
$eventType = $event['type'];
|
||||
|
||||
// make sure the event still exists
|
||||
if (!isset($availableEvents[$eventType])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$settings = $availableEvents[$eventType];
|
||||
|
||||
if (isset($settings['callback']) && is_callable($settings['callback'])) {
|
||||
return $this->invokeCallback($event, $lead, $settings);
|
||||
} else {
|
||||
/** @var TriggerEvent $triggerEvent */
|
||||
$triggerEvent = $this->getEventRepository()->find($event['id']);
|
||||
|
||||
$triggerExecutedEvent = new Events\TriggerExecutedEvent($triggerEvent, $lead);
|
||||
|
||||
$this->dispatcher->dispatch($triggerExecutedEvent, $settings['eventName']);
|
||||
|
||||
return $triggerExecutedEvent->getResult();
|
||||
}
|
||||
}
|
||||
|
||||
private function invokeCallback($event, Lead $lead, array $settings): mixed
|
||||
{
|
||||
$args = [
|
||||
'event' => $event,
|
||||
'lead' => $lead,
|
||||
'config' => $event['properties'],
|
||||
];
|
||||
|
||||
if (is_array($settings['callback'])) {
|
||||
$reflection = new \ReflectionMethod($settings['callback'][0], $settings['callback'][1]);
|
||||
} elseif (str_contains($settings['callback'], '::')) {
|
||||
$parts = explode('::', $settings['callback']);
|
||||
$reflection = new \ReflectionMethod($parts[0], $parts[1]);
|
||||
} else {
|
||||
$reflection = new \ReflectionMethod(null, $settings['callback']);
|
||||
}
|
||||
|
||||
$pass = [];
|
||||
foreach ($reflection->getParameters() as $param) {
|
||||
if (isset($args[$param->getName()])) {
|
||||
$pass[] = $args[$param->getName()];
|
||||
} else {
|
||||
$pass[] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $reflection->invokeArgs($this, $pass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger events for the current lead.
|
||||
*/
|
||||
public function triggerEvents(Lead $lead): void
|
||||
{
|
||||
$points = $lead->getPoints();
|
||||
|
||||
// find all published triggers that is applicable to this points
|
||||
/** @var \Mautic\PointBundle\Entity\TriggerEventRepository $repo */
|
||||
$repo = $this->getEventRepository();
|
||||
$events = $repo->getPublishedByPointTotal($points);
|
||||
$groupEvents = $repo->getPublishedByGroupScore($lead->getGroupScores());
|
||||
$events = array_merge($events, $groupEvents);
|
||||
|
||||
if (!empty($events)) {
|
||||
// get a list of actions that has already been applied to this lead
|
||||
$appliedEvents = $repo->getLeadTriggeredEvents($lead->getId());
|
||||
$ipAddress = $this->ipLookupHelper->getIpAddress();
|
||||
$persist = [];
|
||||
|
||||
foreach ($events as $event) {
|
||||
if (isset($appliedEvents[$event['id']])) {
|
||||
// don't apply the event to the lead if it's already been done
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->triggerEvent($event, $lead, true)) {
|
||||
$log = new LeadTriggerLog();
|
||||
$log->setIpAddress($ipAddress);
|
||||
$log->setEvent($triggerEvent = $this->getEventRepository()->find($event['id']));
|
||||
$log->setLead($lead);
|
||||
$log->setDateFired(new \DateTime());
|
||||
$persist[] = $log;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($persist)) {
|
||||
$this->getEventRepository()->saveEntities($persist);
|
||||
$this->getEventRepository()->detachEntities($persist);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns configured color based on passed in $points.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getColorForLeadPoints($points)
|
||||
{
|
||||
if (!$this->triggers) {
|
||||
$this->triggers = $this->getRepository()->getTriggerColors();
|
||||
}
|
||||
|
||||
foreach ($this->triggers as $trigger) {
|
||||
if ($points >= $trigger['points']) {
|
||||
return $trigger['color'];
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle;
|
||||
|
||||
/**
|
||||
* Events available for PointBundle.
|
||||
*/
|
||||
final class PointEvents
|
||||
{
|
||||
/**
|
||||
* The mautic.point_pre_save event is thrown right before a form is persisted.
|
||||
*
|
||||
* The event listener receives a Mautic\PointBundle\Event\PointEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const POINT_PRE_SAVE = 'mautic.point_pre_save';
|
||||
|
||||
/**
|
||||
* The mautic.point_post_save event is thrown right after a form is persisted.
|
||||
*
|
||||
* The event listener receives a Mautic\PointBundle\Event\PointEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const POINT_POST_SAVE = 'mautic.point_post_save';
|
||||
|
||||
/**
|
||||
* The mautic.point_pre_delete event is thrown before a form is deleted.
|
||||
*
|
||||
* The event listener receives a Mautic\PointBundle\Event\PointEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const POINT_PRE_DELETE = 'mautic.point_pre_delete';
|
||||
|
||||
/**
|
||||
* The mautic.point_post_delete event is thrown after a form is deleted.
|
||||
*
|
||||
* The event listener receives a Mautic\PointBundle\Event\PointEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const POINT_POST_DELETE = 'mautic.point_post_delete';
|
||||
|
||||
/**
|
||||
* The mautic.point_on_build event is thrown before displaying the point builder form to allow adding of custom actions.
|
||||
*
|
||||
* The event listener receives a Mautic\PointBundle\Event\PointBuilderEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const POINT_ON_BUILD = 'mautic.point_on_build';
|
||||
|
||||
/**
|
||||
* The mautic.point_on_action event is thrown to execute a point action.
|
||||
*
|
||||
* The event listener receives a Mautic\PointBundle\Event\PointActionEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const POINT_ON_ACTION = 'mautic.point_on_action';
|
||||
|
||||
/**
|
||||
* The mautic.point_pre_save event is thrown right before a form is persisted.
|
||||
*
|
||||
* The event listener receives a Mautic\PointBundle\Event\TriggerEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TRIGGER_PRE_SAVE = 'mautic.trigger_pre_save';
|
||||
|
||||
/**
|
||||
* The mautic.trigger_post_save event is thrown right after a form is persisted.
|
||||
*
|
||||
* The event listener receives a Mautic\PointBundle\Event\TriggerEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TRIGGER_POST_SAVE = 'mautic.trigger_post_save';
|
||||
|
||||
/**
|
||||
* The mautic.trigger_pre_delete event is thrown before a form is deleted.
|
||||
*
|
||||
* The event listener receives a Mautic\PointBundle\Event\TriggerEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TRIGGER_PRE_DELETE = 'mautic.trigger_pre_delete';
|
||||
|
||||
/**
|
||||
* The mautic.trigger_post_delete event is thrown after a form is deleted.
|
||||
*
|
||||
* The event listener receives a Mautic\PointBundle\Event\TriggerEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TRIGGER_POST_DELETE = 'mautic.trigger_post_delete';
|
||||
|
||||
/**
|
||||
* The mautic.trigger_on_build event is thrown before displaying the trigger builder form to allow adding of custom actions.
|
||||
*
|
||||
* The event listener receives a Mautic\PointBundle\Event\TriggerBuilderEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TRIGGER_ON_BUILD = 'mautic.trigger_on_build';
|
||||
|
||||
/**
|
||||
* The mautic.trigger_on_event_execute event is thrown to execute a trigger event.
|
||||
*
|
||||
* The event listener receives a Mautic\PointBundle\Event\TriggerExecutedEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TRIGGER_ON_EVENT_EXECUTE = 'mautic.trigger_on_event_execute';
|
||||
|
||||
/**
|
||||
* The mautic.trigger_on_lead_segments_change event is thrown to change lead's segments.
|
||||
*
|
||||
* The event listener receives a Mautic\PointBundle\Event\TriggerExecutedEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TRIGGER_ON_LEAD_SEGMENTS_CHANGE = 'mautic.trigger_on_lead_segments_change';
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle;
|
||||
|
||||
final class PointGroupEvents
|
||||
{
|
||||
/**
|
||||
* The mautic.group_pre_save event is thrown right before a form is persisted.
|
||||
*
|
||||
* The event listener receives a Mautic\PointBundle\Event\ScoringCategoryEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const GROUP_PRE_SAVE = 'mautic.group_pre_save';
|
||||
|
||||
/**
|
||||
* The mautic.group_post_save event is thrown right after a form is persisted.
|
||||
*
|
||||
* The event listener receives a Mautic\PointBundle\Event\ScoringCategoryEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const GROUP_POST_SAVE = 'mautic.group_post_save';
|
||||
|
||||
/**
|
||||
* The mautic.group_pre_delete event is thrown before a form is deleted.
|
||||
*
|
||||
* The event listener receives a Mautic\PointBundle\Event\ScoringCategoryEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const GROUP_PRE_DELETE = 'mautic.group_pre_delete';
|
||||
|
||||
/**
|
||||
* The mautic.group_post_delete event is thrown after a form is deleted.
|
||||
*
|
||||
* The event listener receives a Mautic\PointBundle\Event\ScoringCategoryEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const GROUP_POST_DELETE = 'mautic.group_post_delete';
|
||||
|
||||
/**
|
||||
* The mautic.group_contact_score_change event is dispatched if a group contact score changes.
|
||||
*
|
||||
* The event listener receives a Mautic\PointBundle\Event\GroupScoreChangeEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const SCORE_CHANGE = 'mautic.group_contact_score_change';
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{#
|
||||
Variables
|
||||
- deleted
|
||||
- id
|
||||
- route
|
||||
- sessionId
|
||||
#}
|
||||
{% set action = deleted ? 'undelete' : 'delete' %}
|
||||
{% set iconClass = deleted ? 'ri-arrow-go-back-line' : 'ri-close-line' %}
|
||||
{% set btnClass = deleted ? 'btn-warning' : 'btn-danger' %}
|
||||
{% set route = route|default('mautic_pointtriggerevent_action') %}
|
||||
<div class="form-buttons hide">
|
||||
<a data-toggle="ajaxmodal" data-target="#triggerEventModal" href="{{ path(route, {'objectAction': 'edit', 'objectId': id, 'triggerId': sessionId}) }}" class="btn btn-primary btn-xs btn-edit">
|
||||
<i class="ri-edit-line"></i>
|
||||
</a>
|
||||
<a data-menu-link="mautic_point_index" data-toggle="ajax" data-ignore-formexit="true" data-method="POST" data-hide-loadingbar="true" href="{{ path(route, {'objectAction': action, 'objectId': id, 'triggerId': sessionId}) }}" class="btn {{ btnClass }} btn-xs">
|
||||
<i class="{{ iconClass }}"></i>
|
||||
</a>
|
||||
<i class="ri-fw ri-more-2-line reorder-handle"></i>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
{% if formTheme is defined %}
|
||||
{% form_theme form formTheme %}
|
||||
{% endif %}
|
||||
<div class="bundle-form">
|
||||
<div class="bundle-form-header">
|
||||
<h3>{{ eventHeader }}</h3>
|
||||
</div>
|
||||
{{ form(form) }}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
{#
|
||||
Variables
|
||||
- event
|
||||
- id
|
||||
- sessionId
|
||||
- deleted (bool, optional)
|
||||
#}
|
||||
{% set deleted = deleted|default(false) %}
|
||||
<div class="trigger-event-row {% if deleted %}bg-danger{% endif %}" id="triggerEvent_{{ id }}">
|
||||
{{ include('@MauticPoint/Event/actions.html.twig', {
|
||||
'deleted': deleted,
|
||||
'id': id,
|
||||
'route': 'mautic_pointtriggerevent_action',
|
||||
'sessionId': sessionId|default(''),
|
||||
}) }}
|
||||
<span class="trigger-event-label">{{ event.name }}</span>
|
||||
{% if event.description is not empty %}
|
||||
<span class="trigger-event-descr">{{ event.description|purify }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
{% block _pointaction_properties_row %}
|
||||
{{ form_widget(form) }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
|
||||
{% block mauticContent %}group{% endblock %}
|
||||
|
||||
{% block headerTitle %}
|
||||
{% if entity.id %}
|
||||
{{ 'mautic.point.group.menu.edit'|trans }}
|
||||
{% else %}
|
||||
{{ 'mautic.point.group.menu.new'|trans }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{{ form_start(form) }}
|
||||
<!-- start: box layout -->
|
||||
<div class="box-layout">
|
||||
<!-- container -->
|
||||
<div class="col-md-9 height-auto bdr-r">
|
||||
<div class="pa-md">
|
||||
{{ form_row(form.name) }}
|
||||
{{ form_row(form.description) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 height-auto">
|
||||
<div class="pr-lg pl-lg pt-md pb-md">
|
||||
{{ form_row(form.isPublished) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end: box layout -->
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,139 @@
|
||||
{% set isIndex = 'index' == tmpl ? true : false %}
|
||||
{% set tmpl = 'list' %}
|
||||
{% extends isIndex ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
|
||||
{% block mauticContent %}group{% endblock %}
|
||||
|
||||
{% block headerTitle %}{{ 'mautic.point.group.header.index'|trans }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="page-list-wrapper" class="{% if items|length > 0 or searchValue is not empty %}panel {% endif %}panel-default">
|
||||
{{- include('@MauticCore/Helper/list_toolbar.html.twig', {
|
||||
'searchValue': searchValue,
|
||||
'action': currentRoute,
|
||||
'page_actions': {
|
||||
'templateButtons': {
|
||||
'new': permissions['point:groups:create']
|
||||
},
|
||||
'routeBase': 'point.group',
|
||||
'langVar': 'point.group'
|
||||
},
|
||||
'bulk_actions': {
|
||||
'langVar': 'point.group',
|
||||
'routeBase': 'point.group',
|
||||
'templateButtons': {
|
||||
'delete': permissions['point:groups:delete'],
|
||||
},
|
||||
},
|
||||
}) -}}
|
||||
<div class="page-list">
|
||||
{% if items is defined and items is not empty %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover" id="groupTable">
|
||||
<thead>
|
||||
<tr>
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'checkall': 'true',
|
||||
'target': '#groupTable',
|
||||
}) }}
|
||||
{{ include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'sessionVar': 'point.group',
|
||||
'orderBy' : 'pl.name',
|
||||
'text' : 'mautic.core.name',
|
||||
'class' : 'col-group-name',
|
||||
'default' : true,
|
||||
}
|
||||
) }}
|
||||
|
||||
{{ include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'sessionVar' : 'point.group',
|
||||
'orderBy' : 'pl.id',
|
||||
'text' : 'mautic.core.id',
|
||||
'class' : 'visible-md visible-lg col-group-id',
|
||||
}
|
||||
) }}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>
|
||||
{{- include('@MauticCore/Helper/list_actions.html.twig', {
|
||||
item: item,
|
||||
templateButtons: {
|
||||
edit: permissions['point:groups:edit'],
|
||||
delete: permissions['point:groups:delete'],
|
||||
},
|
||||
routeBase: 'point.group',
|
||||
langVar: 'point.group',
|
||||
}) -}}
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
{{- include(
|
||||
'@MauticCore/Helper/publishstatus_icon.html.twig',
|
||||
{
|
||||
'item': item, 'model': 'point.group'
|
||||
}
|
||||
)}}
|
||||
|
||||
{% if permissions['point:groups:edit'] %}
|
||||
<a href="{{ path('mautic_point.group_action', {
|
||||
objectAction: 'edit',
|
||||
objectId: item.getId()
|
||||
}) }}" data-toggle="ajax">
|
||||
{{ item.getName() }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ item.getName() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% set description = item.getDescription() %}
|
||||
{{ include('@MauticCore/Helper/description--inline.html.twig', {
|
||||
'description': item.description
|
||||
}) }}
|
||||
</td>
|
||||
<td class="visible-md visible-lg">{{ item.getId() }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer">
|
||||
{{- include('@MauticCore/Helper/pagination.html.twig', {
|
||||
'totalItems' : items|length,
|
||||
'page' : page,
|
||||
'limit' : limit,
|
||||
'baseUrl' : path('mautic_point.group_index'),
|
||||
'sessionVar' : 'point.group',
|
||||
}) -}}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if searchValue is not empty %}
|
||||
{{ include('@MauticCore/Helper/noresults.html.twig') }}
|
||||
{% else %}
|
||||
<div class="mt-80 col-md-offset-2 col-lg-offset-3 col-md-8 col-lg-5 height-auto">
|
||||
{% set childContainer %}
|
||||
<div class="mb-md">
|
||||
{% include '@MauticCore/Components/pictogram.html.twig' with {
|
||||
'pictogram': 'filter-and-group-data',
|
||||
'size': '80'
|
||||
} %}
|
||||
</div>
|
||||
{% endset %}
|
||||
|
||||
{{ include('@MauticCore/Components/content-block.html.twig', {
|
||||
heading: 'mautic.point.group.onboarding.heading',
|
||||
subheading: 'mautic.point.group.onboarding.subheading',
|
||||
copy: 'mautic.point.group.onboarding.copy',
|
||||
childContainer: childContainer,
|
||||
}) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,141 @@
|
||||
{% if items|length > 0 %}
|
||||
<div class="table-responsive page-list">
|
||||
<table class="table table-hover point-list" id="pointTable">
|
||||
<thead>
|
||||
<tr>
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'checkall': 'true',
|
||||
'target': '#pointTable',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'point',
|
||||
'orderBy': 'p.name',
|
||||
'text': 'mautic.core.name',
|
||||
'class': 'col-point-name',
|
||||
'default': true,
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'point',
|
||||
'orderBy': 'cat.title',
|
||||
'text': 'mautic.core.category',
|
||||
'class': 'visible-md visible-lg col-point-category',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'point',
|
||||
'orderBy': 'pl.name',
|
||||
'text': 'mautic.point.thead.group',
|
||||
'class': 'visible-md visible-lg col-point-group',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'point',
|
||||
'orderBy': 'p.delta',
|
||||
'text': 'mautic.point.thead.delta',
|
||||
'class': 'visible-md visible-lg col-point-delta',
|
||||
}) }}
|
||||
|
||||
<th class="col-point-action">{{ 'mautic.point.thead.action'|trans }}</th>
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'point',
|
||||
'orderBy': 'p.id',
|
||||
'text': 'mautic.core.id',
|
||||
'class': 'visible-md visible-lg col-point-id',
|
||||
}) }}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ include('@MauticCore/Helper/list_actions.html.twig', {
|
||||
'item': item,
|
||||
'templateButtons': {
|
||||
'edit': permissions['point:points:edit'],
|
||||
'clone': permissions['point:points:create'],
|
||||
'delete': permissions['point:points:delete'],
|
||||
},
|
||||
'routeBase': 'point',
|
||||
}) }}
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
{{ include('@MauticCore/Helper/publishstatus_icon.html.twig', {'item': item, 'model': 'point'}) }}
|
||||
{% if permissions['point:points:edit'] %}
|
||||
<a href="{{ path('mautic_point_action', {'objectAction': 'edit', 'objectId': item.id}) }}" data-toggle="ajax">
|
||||
{{ item.name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ item.name }}
|
||||
{% endif %}
|
||||
{{ customContent('point.name', _context) }}
|
||||
</div>
|
||||
{{ include('@MauticCore/Helper/description--inline.html.twig', {
|
||||
'description': item.description
|
||||
}) }}
|
||||
</td>
|
||||
<td class="visible-md visible-lg">
|
||||
{{ include('@MauticCore/Modules/category--expanded.html.twig', {'category': item.category}) }}
|
||||
</td>
|
||||
<td class="visible-md visible-lg">
|
||||
{% set group = item.group %}
|
||||
{% set groupName = group.name|default('mautic.point.group.form.nogroup'|trans) %}
|
||||
{{ groupName }}
|
||||
</td>
|
||||
<td class="visible-md visible-lg">{{ item.delta }}</td>
|
||||
<td>{{ actions[item.type].label|default('')|trans }}</td>
|
||||
<td class="visible-md visible-lg">{{ item.id }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
{{ include('@MauticCore/Helper/pagination.html.twig', {
|
||||
'totalItems': items|length,
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
'menuLinkId': 'mautic_point_index',
|
||||
'baseUrl': path('mautic_point_index'),
|
||||
'sessionVar': 'point',
|
||||
}) }}
|
||||
</div>
|
||||
{% else %}
|
||||
{% if searchValue is not empty %}
|
||||
{{ include('@MauticCore/Helper/noresults.html.twig', {'tip': 'mautic.point.action.noresults.tip'}) }}
|
||||
{% else %}
|
||||
<div class="mt-80 col-md-offset-2 col-lg-offset-3 col-md-8 col-lg-5 height-auto">
|
||||
{% set childContainer %}
|
||||
<div class="mb-md">
|
||||
{% include '@MauticCore/Components/pictogram.html.twig' with {
|
||||
'pictogram': 'user--insights',
|
||||
'size': '80'
|
||||
} %}
|
||||
</div>
|
||||
|
||||
{{ include('@MauticCore/Components/content-item-row.html.twig', {
|
||||
type: 'default',
|
||||
eyebrow: 'mautic.point.action.empty.understand',
|
||||
heading: 'mautic.point.action.empty.what_are_point_actions',
|
||||
copy: 'mautic.point.action.empty.what_are_point_actions_desc',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Components/content-item-row.html.twig', {
|
||||
type: 'default',
|
||||
eyebrow: 'mautic.point.action.empty.purpose',
|
||||
heading: 'mautic.point.action.empty.score_contacts_heading',
|
||||
copy: 'mautic.point.action.empty.score_contacts_desc',
|
||||
}) }}
|
||||
{% endset %}
|
||||
|
||||
{{ include('@MauticCore/Components/content-block.html.twig', {
|
||||
heading: 'mautic.point.action.empty.heading',
|
||||
subheading: 'mautic.point.action.empty.subheading',
|
||||
childContainer: childContainer,
|
||||
}) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% form_theme form with formThemes %}
|
||||
{% for child in form %}
|
||||
{{ form_row(child) }}
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,54 @@
|
||||
{% form_theme form with formThemes %}
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
|
||||
{% block mauticContent 'point' %}
|
||||
|
||||
{%- block headerTitle -%}
|
||||
{%- if entity.id -%}
|
||||
{{- 'mautic.point.menu.edit'|trans({'%name%': entity.name|trans}) -}}
|
||||
{%- else -%}
|
||||
{{- 'mautic.point.menu.new'|trans -}}
|
||||
{%- endif -%}
|
||||
{%- endblock -%}
|
||||
|
||||
{% block content %}
|
||||
{{ form_start(form) }}
|
||||
<!-- start: box layout -->
|
||||
<div class="box-layout">
|
||||
<!-- container -->
|
||||
<div class="col-md-9 height-auto bdr-r">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="pa-md">
|
||||
{{ form_row(form.name) }}
|
||||
{{ form_row(form.description) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="pa-md">
|
||||
{{ form_row(form.delta) }}
|
||||
{{ form_row(form.type) }}
|
||||
<div id="pointActionProperties">
|
||||
{% if form.properties is defined %}
|
||||
{{ form_row(form.properties) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ form_row(form.group) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 height-auto">
|
||||
<div class="pr-lg pl-lg pt-md pb-md">
|
||||
{{ form_row(form.category) }}
|
||||
{{ form_row(form.projects) }}
|
||||
{{ form_row(form.isPublished) }}
|
||||
{{ form_row(form.repeatable) }}
|
||||
{{ form_row(form.publishUp) }}
|
||||
{{ form_row(form.publishDown) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end: box layout -->
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,67 @@
|
||||
{% set isIndex = 'index' == tmpl %}
|
||||
{% set tmpl = 'list' %}
|
||||
{% extends isIndex ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
|
||||
|
||||
{% block mauticContent 'point' %}
|
||||
|
||||
{% block headerTitle 'mautic.points.menu.root'|trans %}
|
||||
|
||||
{% block content %}
|
||||
{% if isIndex %}
|
||||
<div id="page-list-wrapper" class="{% if items|length > 0 or searchValue is not empty %}panel {% endif %}panel-default">
|
||||
{{ include('@MauticCore/Helper/list_toolbar.html.twig', {
|
||||
'searchValue': searchValue,
|
||||
'action': currentRoute,
|
||||
'page_actions': {
|
||||
'templateButtons': {
|
||||
'new': permissions['point:points:create'],
|
||||
},
|
||||
'routeBase': 'point',
|
||||
},
|
||||
'bulk_actions': {
|
||||
'routeBase': 'point',
|
||||
'templateButtons': {
|
||||
'delete': permissions['point:points:delete'],
|
||||
},
|
||||
},
|
||||
'quickFilters': [
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.ispublished',
|
||||
'label': 'mautic.core.form.active',
|
||||
'tooltip': 'mautic.core.searchcommand.ispublished.description',
|
||||
'icon': 'ri-check-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.isunpublished',
|
||||
'label': 'mautic.core.form.inactive',
|
||||
'tooltip': 'mautic.core.searchcommand.isunpublished.description',
|
||||
'icon': 'ri-close-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.isuncategorized',
|
||||
'label': 'mautic.core.form.uncategorized',
|
||||
'tooltip': 'mautic.core.searchcommand.isuncategorized.description',
|
||||
'icon': 'ri-folder-unknow-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.ismine',
|
||||
'label': 'mautic.core.searchcommand.ismine.label',
|
||||
'tooltip': 'mautic.core.searchcommand.ismine.description',
|
||||
'icon': 'ri-user-line'
|
||||
}
|
||||
]
|
||||
}) }}
|
||||
<div class="page-list">
|
||||
{% endif %}
|
||||
|
||||
{{ include('@MauticPoint/Point/_list.html.twig') }}
|
||||
|
||||
{% if isIndex %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ include('@MauticCore/Modules/protip.html.twig', {
|
||||
tip: random(['mautic.protip.points.decay', 'mautic.protip.points.reduce', 'mautic.protip.points.limit'])
|
||||
}) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,16 @@
|
||||
{% if showMore is defined %}
|
||||
<a href="{{ url('mautic_point.group_index', {'search': searchString}) }}" data-toggle="ajax">
|
||||
<span>{{ 'mautic.core.search.more'|trans({'%count%': remaining}) }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url('mautic_point.group_action', {'objectAction': 'edit', 'objectId': item.id}) }}">
|
||||
<span class="fw-sb">{{ item.name }}</span>
|
||||
<span class="ml-4 mr-sm">#{{ item.id }}</span>
|
||||
{{- include('@MauticCore/Helper/publishstatus_badge.html.twig', {
|
||||
'entity': item,
|
||||
'status': 'active',
|
||||
'simplified': 'true'
|
||||
}) -}}
|
||||
</a>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,17 @@
|
||||
{% if showMore is defined %}
|
||||
<a href="{{ url('mautic_point_index', {'search': searchString}) }}" data-toggle="ajax">
|
||||
<span>{{ 'mautic.core.search.more'|trans({'%count%': remaining}) }}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="{{ url('mautic_point_action', {'objectAction': 'edit', 'objectId': item.id}) }}">
|
||||
<span class="fw-sb">{{ item.name }}</span>
|
||||
<span class="ml-4 mr-sm">#{{ item.id }}</span>
|
||||
{{- include('@MauticCore/Helper/publishstatus_badge.html.twig', {
|
||||
'entity': item,
|
||||
'status': 'active',
|
||||
'simplified': 'true'
|
||||
}) -}}
|
||||
</a>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,16 @@
|
||||
{% if showMore is defined %}
|
||||
<a href="{{ url('mautic_pointtrigger_index', {'search': searchString}) }}" data-toggle="ajax">
|
||||
<span>{{ 'mautic.core.search.more'|trans({'%count%': remaining}) }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url('mautic_pointtrigger_action', {'objectAction': 'edit', 'objectId': item.id}) }}">
|
||||
<span class="fw-sb">{{ item.name }}</span>
|
||||
<span class="ml-4 mr-sm">#{{ item.id }}</span>
|
||||
{{- include('@MauticCore/Helper/publishstatus_badge.html.twig', {
|
||||
'entity': item,
|
||||
'status': 'active',
|
||||
'simplified': 'true'
|
||||
}) -}}
|
||||
</a>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,170 @@
|
||||
{% if items|length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover pointtrigger-list" id="triggerTable">
|
||||
<thead>
|
||||
<tr>
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'checkall': 'true',
|
||||
'target': '#triggerTable',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'point.trigger',
|
||||
'orderBy': 't.name',
|
||||
'text': 'mautic.core.name',
|
||||
'class': 'col-pointtrigger-name',
|
||||
'default': true,
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'point.trigger',
|
||||
'orderBy': 'cat.title',
|
||||
'text': 'mautic.core.category',
|
||||
'class': 'col-pointtrigger-category visible-md visible-lg',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'point.trigger',
|
||||
'orderBy': 'pl.name',
|
||||
'text': 'mautic.point.thead.group',
|
||||
'class': 'visible-md visible-lg col-point-group',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'point.trigger',
|
||||
'orderBy': 't.points',
|
||||
'text': 'mautic.point.trigger.thead.points',
|
||||
'class': 'col-pointtrigger-points',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'point.trigger',
|
||||
'orderBy': 't.id',
|
||||
'text': 'mautic.core.id',
|
||||
'class': 'col-pointtrigger-id visible-md visible-lg',
|
||||
}) }}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ include('@MauticCore/Helper/list_actions.html.twig', {
|
||||
'item': item,
|
||||
'templateButtons': {
|
||||
'edit': permissions['point:triggers:edit'],
|
||||
'clone': permissions['point:triggers:create'],
|
||||
'delete': permissions['point:triggers:delete'],
|
||||
},
|
||||
'routeBase': 'pointtrigger',
|
||||
'langVar': 'point.trigger',
|
||||
}) }}
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
{{ include('@MauticCore/Helper/publishstatus_icon.html.twig', {'item': item, 'model': 'point.trigger'}) }}
|
||||
{% if permissions['point:triggers:edit'] %}
|
||||
<a href="{{ path('mautic_pointtrigger_action', {'objectAction': 'edit', 'objectId': item.id}) }}" data-toggle="ajax">
|
||||
{{ item.name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ item.name }}
|
||||
{% endif %}
|
||||
{{ customContent('trigger.name', _context) }}
|
||||
</div>
|
||||
{{ include('@MauticCore/Helper/description--inline.html.twig', {
|
||||
'description': item.description
|
||||
}) }}
|
||||
</td>
|
||||
<td class="visible-md visible-lg">
|
||||
{{ include('@MauticCore/Modules/category--expanded.html.twig', {'category': item.category}) }}
|
||||
</td>
|
||||
<td class="visible-md visible-lg">
|
||||
{{ item.group.name|default('mautic.point.group.form.nogroup'|trans) }}
|
||||
</td>
|
||||
<td>{{ item.points }}</td>
|
||||
<td class="visible-md visible-lg">{{ item.id }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
{{ include('@MauticCore/Helper/pagination.html.twig', {
|
||||
'totalItems': items|length,
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
'menuLinkId': 'mautic_pointtrigger_index',
|
||||
'baseUrl': path('mautic_pointtrigger_index'),
|
||||
'sessionVar': 'point.trigger',
|
||||
}) }}
|
||||
</div>
|
||||
{% else %}
|
||||
{% if searchValue is not empty %}
|
||||
{{ include('@MauticCore/Helper/noresults.html.twig', {'tip': 'mautic.point.trigger.noresults.tip'}) }}
|
||||
{% else %}
|
||||
<div class="mt-80 col-md-offset-2 col-lg-offset-3 col-md-8 col-lg-5 height-auto">
|
||||
{% set childContainer %}
|
||||
<div class="mb-md">
|
||||
{% include '@MauticCore/Components/pictogram.html.twig' with {
|
||||
'pictogram': 'react-to-data',
|
||||
'size': '80'
|
||||
} %}
|
||||
</div>
|
||||
|
||||
{{ include('@MauticCore/Components/content-item-row.html.twig', {
|
||||
type: 'default',
|
||||
eyebrow: 'mautic.point.trigger.onboarding.eyebrow',
|
||||
heading: 'mautic.point.trigger.onboarding.heading',
|
||||
copy: 'mautic.point.trigger.onboarding.copy',
|
||||
}) }}
|
||||
|
||||
{% set triggersUseContainer %}
|
||||
<div class="mb-md">
|
||||
{% include '@MauticCore/Components/pictogram.html.twig' with {
|
||||
'pictogram': 'construct',
|
||||
'size': '64'
|
||||
} %}
|
||||
</div>
|
||||
<div class="type-body-02">
|
||||
{% include '@MauticCore/Components/list--styled.html.twig' with {
|
||||
'type': 'ordered',
|
||||
'style': 'numbers',
|
||||
'items': [
|
||||
'mautic.point.trigger.onboarding.step1',
|
||||
'mautic.point.trigger.onboarding.step2',
|
||||
'mautic.point.trigger.onboarding.step3',
|
||||
]
|
||||
} %}
|
||||
</div>
|
||||
<div class="mt-lg">
|
||||
{{ include('@MauticCore/Notification/inline_notification.html.twig', {
|
||||
'title': 'mautic.point.trigger.onboarding.notification.title',
|
||||
'content': 'mautic.point.trigger.onboarding.notification.content',
|
||||
'alert_type': 'success',
|
||||
'dismissible': false,
|
||||
}) }}
|
||||
</div>
|
||||
{% endset %}
|
||||
|
||||
{{ include('@MauticCore/Components/content-group.html.twig', {
|
||||
heading: 'mautic.point.trigger.onboarding.group.heading',
|
||||
childContainer: triggersUseContainer,
|
||||
cta: {
|
||||
'label': 'mautic.point.trigger.onboarding.cta',
|
||||
'link': path('mautic_pointtrigger_action', {'objectAction': 'new'}),
|
||||
attributes: {
|
||||
'data-toggle': 'ajax',
|
||||
},
|
||||
}
|
||||
}) }}
|
||||
{% endset %}
|
||||
|
||||
{{ include('@MauticCore/Components/content-block.html.twig', {
|
||||
heading: 'mautic.point.trigger.empty.heading',
|
||||
subheading: 'mautic.point.trigger.empty.subheading',
|
||||
childContainer: childContainer,
|
||||
}) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,23 @@
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
|
||||
{% block mauticContent 'pointTrigger' %}
|
||||
|
||||
{% block headerTitle entity.name %}
|
||||
|
||||
{% block actions %}
|
||||
{{ include('@MauticCore/Helper/page_actions.html.twig', {
|
||||
'item': entity,
|
||||
'templateButtons': {
|
||||
'edit': permissions['point:triggers:edit'],
|
||||
'delete': permissions['point:triggers:delete'],
|
||||
},
|
||||
'routeBase': 'pointtrigger',
|
||||
'langVar': 'point.trigger',
|
||||
}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="scrollable trigger-details">
|
||||
{# @todo - output trigger details/actions #}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,114 @@
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
|
||||
{% block mauticContent 'pointTrigger' %}
|
||||
|
||||
{%- block headerTitle -%}
|
||||
{%- if entity.id -%}
|
||||
{{ 'mautic.point.trigger.header.edit'|trans({'%name%': entity.name|trans}) }}
|
||||
{%- else -%}
|
||||
{{ 'mautic.point.trigger.header.new'|trans }}
|
||||
{%- endif -%}
|
||||
{%- endblock -%}
|
||||
|
||||
{% block modal include('@MauticCore/Helper/modal.html.twig', {
|
||||
'id': 'triggerEventModal',
|
||||
'header': 'mautic.point.trigger.form.modalheader'|trans,
|
||||
'footerButtons': true
|
||||
}) %}
|
||||
|
||||
{% block content %}
|
||||
{{ form_start(form) }}
|
||||
<div class="box-layout">
|
||||
<div class="col-md-9 height-auto">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<!-- tabs controls -->
|
||||
<ul class="nav nav-tabs nav-tabs-contained">
|
||||
<li class="active"><a href="#details-container" role="tab" data-toggle="tab">{{ 'mautic.core.details'|trans }}</a></li>
|
||||
<li class=""><a href="#events-container" role="tab" data-toggle="tab">{{ 'mautic.point.trigger.tab.events'|trans }}</a></li>
|
||||
</ul>
|
||||
<!--/ tabs controls -->
|
||||
|
||||
<div class="tab-content pa-md">
|
||||
<div class="tab-pane fade in active bdr-w-0 height-auto" id="details-container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="pa-md">
|
||||
{{ form_row(form.name) }}
|
||||
{{ form_row(form.description, {'attr': {'class': 'form-control editor'}}) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="pa-md">
|
||||
{{ form_row(form.points) }}
|
||||
{{ form_row(form.color) }}
|
||||
{{ form_row(form.group) }}
|
||||
{{ form_row(form.triggerExistingLeads) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade bdr-w-0" id="events-container">
|
||||
<div id="triggerEvents">
|
||||
<div class="mb-md">
|
||||
<div class="dropdown chosen-container">
|
||||
<button class="btn chosen-single dropdown-toggle" type="button" data-toggle="dropdown">
|
||||
{{ 'mautic.point.trigger.event.add'|trans }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
{% for group, event in events %}
|
||||
<li role="presentation" class="dropdown-header">
|
||||
{{ group }}
|
||||
</li>
|
||||
{% for k, e in event %}
|
||||
<li id="event_{{ k }}">
|
||||
<a data-toggle="ajaxmodal"
|
||||
data-target="#triggerEventModal"
|
||||
class="list-group-item"
|
||||
href="{{ path('mautic_pointtriggerevent_action', {'objectAction': 'new', 'type': k, 'tmpl': 'event', 'triggerId': sessionId}) }}">
|
||||
<div data-toggle="tooltip" title="{{ e.description }}">
|
||||
<span>{{ e.label }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% for event in triggerEvents %}
|
||||
{% set template = event.settings.template|default('@MauticPoint/Event/generic.html.twig') %}
|
||||
<!-- start: "{{ template }}" -->
|
||||
{{ include(template, {
|
||||
'event': event,
|
||||
'id': event.id,
|
||||
'deleted': (event.id in deletedEvents),
|
||||
'sessionId': sessionId,
|
||||
}, with_context=false) }}
|
||||
<!--/ start: "{{ template }}" -->
|
||||
{% else %}
|
||||
<div class="alert alert-info" id="triggerEventPlaceholder">
|
||||
<p>{{ 'mautic.point.trigger.addevent'|trans }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 height-auto bdr-l">
|
||||
<div class="pr-lg pl-lg pt-md pb-md">
|
||||
{{ form_row(form.category) }}
|
||||
{{ form_row(form.projects) }}
|
||||
{{ form_row(form.isPublished) }}
|
||||
{{ form_row(form.publishUp) }}
|
||||
{{ form_row(form.publishDown) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,69 @@
|
||||
{% set isIndex = 'index' == tmpl %}
|
||||
{% set tmpl = 'list' %}
|
||||
{% extends isIndex ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
|
||||
|
||||
{% block mauticContent 'pointTrigger' %}
|
||||
|
||||
{% block headerTitle 'mautic.point.trigger.header.index'|trans %}
|
||||
|
||||
{% block content %}
|
||||
{% if isIndex %}
|
||||
<div id="page-list-wrapper" class="{% if items|length > 0 or searchValue is not empty %}panel {% endif %}panel-default">
|
||||
{{ include('@MauticCore/Helper/list_toolbar.html.twig', {
|
||||
'searchValue': searchValue,
|
||||
'action': currentRoute,
|
||||
'page_actions': {
|
||||
'templateButtons': {
|
||||
'new': permissions['point:triggers:create'],
|
||||
},
|
||||
'routeBase': 'pointtrigger',
|
||||
'langVar': 'point.trigger',
|
||||
},
|
||||
'bulk_actions': {
|
||||
'langVar': 'point.trigger',
|
||||
'routeBase': 'pointtrigger',
|
||||
'templateButtons': {
|
||||
'delete': permissions['point:triggers:delete'],
|
||||
},
|
||||
},
|
||||
'quickFilters': [
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.ispublished',
|
||||
'label': 'mautic.core.form.active',
|
||||
'tooltip': 'mautic.core.searchcommand.ispublished.description',
|
||||
'icon': 'ri-check-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.isunpublished',
|
||||
'label': 'mautic.core.form.inactive',
|
||||
'tooltip': 'mautic.core.searchcommand.isunpublished.description',
|
||||
'icon': 'ri-close-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.isuncategorized',
|
||||
'label': 'mautic.core.form.uncategorized',
|
||||
'tooltip': 'mautic.core.searchcommand.isuncategorized.description',
|
||||
'icon': 'ri-folder-unknow-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.ismine',
|
||||
'label': 'mautic.core.searchcommand.ismine.label',
|
||||
'tooltip': 'mautic.core.searchcommand.ismine.description',
|
||||
'icon': 'ri-user-line'
|
||||
}
|
||||
]
|
||||
}) }}
|
||||
<div class="page-list">
|
||||
{% endif %}
|
||||
|
||||
{{ include('@MauticPoint/Trigger/_list.html.twig') }}
|
||||
|
||||
{% if isIndex %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ include('@MauticCore/Modules/protip.html.twig', {
|
||||
tip: random(['mautic.protip.triggers.behavioral'])
|
||||
}) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Security\Permissions;
|
||||
|
||||
use Mautic\CoreBundle\Security\Permissions\AbstractPermissions;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class PointPermissions extends AbstractPermissions
|
||||
{
|
||||
public function __construct($params)
|
||||
{
|
||||
parent::__construct($params);
|
||||
|
||||
$this->addStandardPermissions(['points', 'triggers', 'groups', 'categories']);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'point';
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface &$builder, array $options, array $data): void
|
||||
{
|
||||
$this->addStandardFormFields('point', 'categories', $builder, $data);
|
||||
$this->addStandardFormFields('point', 'points', $builder, $data);
|
||||
$this->addStandardFormFields('point', 'triggers', $builder, $data);
|
||||
$this->addStandardFormFields('point', 'groups', $builder, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Tests\Controller\Api;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\PointsChangeLog;
|
||||
use Mautic\PointBundle\Entity\Group;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class PointGroupsApiControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testPointGroupCRUDActions(): void
|
||||
{
|
||||
/** @var Translator $translator */
|
||||
$translator = static::getContainer()->get('translator');
|
||||
|
||||
// Create a new point group
|
||||
$this->client->request('POST', '/api/points/groups/new', [
|
||||
'name' => 'New Point Group',
|
||||
'description' => 'Description of the new point group',
|
||||
]);
|
||||
|
||||
$createResponse = $this->client->getResponse();
|
||||
|
||||
$this->assertSame(Response::HTTP_CREATED, $createResponse->getStatusCode());
|
||||
$responseData = json_decode($createResponse->getContent(), true);
|
||||
$this->assertArrayHasKey('pointGroup', $responseData);
|
||||
$createdData = $responseData['pointGroup'];
|
||||
$this->assertArrayHasKey('id', $createdData);
|
||||
$this->assertEquals('New Point Group', $createdData['name']);
|
||||
$this->assertEquals('Description of the new point group', $createdData['description']);
|
||||
|
||||
// Retrieve all point groups
|
||||
$this->client->request('GET', '/api/points/groups');
|
||||
$getAllResponse = $this->client->getResponse();
|
||||
|
||||
$this->assertSame(Response::HTTP_OK, $getAllResponse->getStatusCode());
|
||||
$responseData = json_decode($getAllResponse->getContent(), true);
|
||||
$this->assertArrayHasKey('pointGroups', $responseData);
|
||||
$this->assertEquals(1, $responseData['total']);
|
||||
$allData = $responseData['pointGroups'];
|
||||
$this->assertIsArray($allData);
|
||||
$this->assertArrayHasKey(0, $allData); // Ensure the response is array-indexed from 0
|
||||
$this->assertCount(1, $allData);
|
||||
|
||||
// Update the created point group
|
||||
$updatePayload = [
|
||||
'name' => 'Updated Point Group Name',
|
||||
'description' => 'Updated description of the point group',
|
||||
];
|
||||
|
||||
$this->client->request('PATCH', "/api/points/groups/{$createdData['id']}/edit", $updatePayload);
|
||||
$updateResponse = $this->client->getResponse();
|
||||
|
||||
$this->assertSame(Response::HTTP_OK, $updateResponse->getStatusCode());
|
||||
$responseData = json_decode($updateResponse->getContent(), true);
|
||||
$this->assertArrayHasKey('pointGroup', $responseData);
|
||||
$updatedData = $responseData['pointGroup'];
|
||||
$this->assertEquals('Updated Point Group Name', $updatedData['name']);
|
||||
$this->assertEquals('Updated description of the point group', $updatedData['description']);
|
||||
|
||||
// Delete the created point group
|
||||
$this->client->request('DELETE', "/api/points/groups/{$createdData['id']}/delete");
|
||||
$deleteResponse = $this->client->getResponse();
|
||||
|
||||
$this->assertSame(Response::HTTP_OK, $deleteResponse->getStatusCode());
|
||||
$responseData = json_decode($deleteResponse->getContent(), true);
|
||||
$this->assertArrayHasKey('pointGroup', $responseData);
|
||||
$deleteData = $responseData['pointGroup'];
|
||||
$this->assertEquals('Updated Point Group Name', $deleteData['name']);
|
||||
$this->assertEquals('Updated description of the point group', $deleteData['description']);
|
||||
|
||||
// Try to GET the group that should no longer exist
|
||||
$this->client->request('GET', "/api/points/groups/{$createdData['id']}");
|
||||
$getResponse = $this->client->getResponse();
|
||||
$this->assertSame(Response::HTTP_NOT_FOUND, $getResponse->getStatusCode());
|
||||
$responseData = json_decode($getResponse->getContent(), true);
|
||||
$this->assertArrayHasKey('errors', $responseData);
|
||||
$this->assertCount(1, $responseData['errors']);
|
||||
$this->assertSame(Response::HTTP_NOT_FOUND, $responseData['errors'][0]['code']);
|
||||
$this->assertSame($translator->trans('mautic.core.error.notfound', [], 'flashes'), $responseData['errors'][0]['message']);
|
||||
}
|
||||
|
||||
public function testContactGroupPointsActions(): void
|
||||
{
|
||||
/** @var Translator $translator */
|
||||
$translator = static::getContainer()->get('translator');
|
||||
|
||||
// Arrange
|
||||
$contact = $this->createContact('test@example.com');
|
||||
$pointGroupA = $this->createGroup('Group A');
|
||||
$pointGroupB = $this->createGroup('Group B');
|
||||
$this->em->flush();
|
||||
|
||||
// Act & Assert
|
||||
$this->adjustPointsAndAssert($contact, $pointGroupA, 'plus', 10, 10);
|
||||
$this->adjustPointsAndAssert($contact, $pointGroupA, 'minus', 2, 8);
|
||||
$this->adjustPointsAndAssert($contact, $pointGroupA, 'divide', 2, 4);
|
||||
$this->adjustPointsAndAssert($contact, $pointGroupA, 'times', 4, 16);
|
||||
$this->adjustPointsAndAssert($contact, $pointGroupB, 'set', 21, 21);
|
||||
|
||||
// Test GET all contact's point groups endpoint
|
||||
$this->assertContactPointGroups($contact, [
|
||||
[
|
||||
'score' => 16,
|
||||
'group' => [
|
||||
'id' => $pointGroupA->getId(),
|
||||
'name' => 'Group A',
|
||||
'description' => '',
|
||||
],
|
||||
],
|
||||
[
|
||||
'score' => 21,
|
||||
'group' => [
|
||||
'id' => $pointGroupB->getId(),
|
||||
'name' => 'Group B',
|
||||
'description' => '',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Test GET single contact's point group endpoint
|
||||
$this->assertContactSinglePointGroup($contact, $pointGroupA, 16);
|
||||
$this->assertContactSinglePointGroup($contact, $pointGroupB, 21);
|
||||
|
||||
$this->assertPointsChangeLogEntries($contact, [
|
||||
['delta' => 10, 'groupId' => $pointGroupA->getId()],
|
||||
['delta' => -2, 'groupId' => $pointGroupA->getId()],
|
||||
['delta' => -4, 'groupId' => $pointGroupA->getId()],
|
||||
['delta' => 12, 'groupId' => $pointGroupA->getId()],
|
||||
['delta' => 21, 'groupId' => $pointGroupB->getId()],
|
||||
]);
|
||||
|
||||
// Try to GET the group points that should not exist
|
||||
$this->client->request('GET', "/api/contacts/{$contact->getId()}/points/groups/0");
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertSame(Response::HTTP_NOT_FOUND, $response->getStatusCode());
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->assertArrayHasKey('errors', $responseData);
|
||||
$this->assertCount(1, $responseData['errors']);
|
||||
$this->assertSame(Response::HTTP_NOT_FOUND, $responseData['errors'][0]['code']);
|
||||
$this->assertSame($translator->trans('mautic.lead.event.api.point.group.not.found'), $responseData['errors'][0]['message']);
|
||||
|
||||
// Try to GET the group points for a contact that should not exist
|
||||
$this->client->request('GET', '/api/contacts/0/points/groups/0');
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertSame(Response::HTTP_NOT_FOUND, $response->getStatusCode());
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->assertArrayHasKey('errors', $responseData);
|
||||
$this->assertCount(1, $responseData['errors']);
|
||||
$this->assertSame(Response::HTTP_NOT_FOUND, $responseData['errors'][0]['code']);
|
||||
$this->assertSame($translator->trans('mautic.lead.event.api.lead.not.found'), $responseData['errors'][0]['message']);
|
||||
}
|
||||
|
||||
private function adjustPointsAndAssert(Lead $contact, Group $pointGroup, string $operator, int $value, int $expectedScore): void
|
||||
{
|
||||
$this->client->request('POST', "/api/contacts/{$contact->getId()}/points/groups/{$pointGroup->getId()}/$operator/{$value}");
|
||||
$adjustResponse = $this->client->getResponse();
|
||||
$this->assertSame(Response::HTTP_OK, $adjustResponse->getStatusCode());
|
||||
$responseData = json_decode($adjustResponse->getContent(), true);
|
||||
$this->assertSame($expectedScore, $responseData['groupScore']['score']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $expectedGroups
|
||||
*/
|
||||
private function assertContactPointGroups(Lead $contact, array $expectedGroups): void
|
||||
{
|
||||
$this->client->request('GET', "/api/contacts/{$contact->getId()}/points/groups");
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertSame(Response::HTTP_OK, $response->getStatusCode());
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->assertSame(count($expectedGroups), $responseData['total']);
|
||||
$this->assertSame($expectedGroups, $responseData['groupScores']);
|
||||
}
|
||||
|
||||
private function assertContactSinglePointGroup(Lead $contact, Group $pointGroup, int $expectedScore): void
|
||||
{
|
||||
$this->client->request('GET', "/api/contacts/{$contact->getId()}/points/groups/{$pointGroup->getId()}");
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertSame(Response::HTTP_OK, $response->getStatusCode());
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->assertSame($expectedScore, $responseData['groupScore']['score']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $expectedEntries
|
||||
*/
|
||||
private function assertPointsChangeLogEntries(Lead $contact, array $expectedEntries): void
|
||||
{
|
||||
$logs = $this->em->getRepository(PointsChangeLog::class)->findBy(['lead' => $contact->getId()]);
|
||||
$this->assertCount(count($expectedEntries), $logs);
|
||||
foreach ($expectedEntries as $index => $expectedEntry) {
|
||||
$this->assertEquals($expectedEntry['delta'], $logs[$index]->getDelta());
|
||||
$this->assertEquals($expectedEntry['groupId'], $logs[$index]->getGroup()->getId());
|
||||
}
|
||||
}
|
||||
|
||||
private function createContact(string $email): Lead
|
||||
{
|
||||
$contact = new Lead();
|
||||
$contact->setEmail($email);
|
||||
$this->em->persist($contact);
|
||||
|
||||
return $contact;
|
||||
}
|
||||
|
||||
private function createGroup(
|
||||
string $name,
|
||||
): Group {
|
||||
$group = new Group();
|
||||
$group->setName($name);
|
||||
$this->em->persist($group);
|
||||
|
||||
return $group;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class PointControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testIndexActionWithoutPage(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/points');
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testIndexActionWithPage(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/points/1');
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testNewAction(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/points/new');
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\PointBundle\Model\TriggerModel;
|
||||
use Mautic\PointBundle\Tests\Functional\TriggerTrait;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class TriggerControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
use TriggerTrait;
|
||||
|
||||
public function testIndexActionWithoutPage(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/points/triggers');
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testIndexActionWithPage(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/points/triggers/1');
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testCloneAction(): void
|
||||
{
|
||||
/** @var TriggerModel $triggerModel */
|
||||
$triggerModel = self::getContainer()->get('mautic.point.model.trigger');
|
||||
|
||||
$triggerRepo = $triggerModel->getRepository();
|
||||
$triggerEventRepo = $triggerModel->getEventRepository();
|
||||
|
||||
$trigger = $this->createTrigger('Trigger', 5);
|
||||
$this->createAddTagEvent('tag1', $trigger);
|
||||
$this->createAddTagEvent('tag2', $trigger);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->assertCount(1, $triggerRepo->findAll());
|
||||
$this->assertCount(2, $triggerEventRepo->findAll());
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/points/triggers/clone/'.$trigger->getId());
|
||||
$this->assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$this->client->submit($form);
|
||||
|
||||
$this->assertCount(2, $triggerRepo->findAll());
|
||||
$this->assertCount(4, $triggerEventRepo->findAll());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Tests\Entity;
|
||||
|
||||
use Doctrine\Persistence\Mapping\MappingException;
|
||||
use Mautic\CoreBundle\Helper\IntHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\PointBundle\Entity\Point;
|
||||
use Symfony\Component\DomCrawler\Form;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class PointEntityValidationTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @throws MappingException
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('deltaScenariosProvider')]
|
||||
public function testDeltaValidationOnCreate(int $delta, string $errorMessage = ''): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/points/new');
|
||||
$buttonCrawler = $crawler->selectButton('Save & Close');
|
||||
$form = $buttonCrawler->form();
|
||||
$form['point[name]']->setValue('Add point');
|
||||
$this->testPointData($form, $delta, $errorMessage);
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('deltaScenariosProvider')]
|
||||
public function testDeltaValidationOnCreateViaAPI(int $delta, string $errorMessage = ''): void
|
||||
{
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
'/api/points/new',
|
||||
[
|
||||
'name' => 'Point1',
|
||||
'delta' => $delta,
|
||||
'isPublished' => true,
|
||||
'type' => 'form.submit',
|
||||
]
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
if ($errorMessage) {
|
||||
self::assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST);
|
||||
self::assertStringContainsString('error', $response->getContent());
|
||||
self::assertStringContainsString($errorMessage, $response->getContent());
|
||||
} else {
|
||||
self::assertResponseStatusCodeSame(Response::HTTP_CREATED);
|
||||
self::assertStringNotContainsString('error', $response->getContent());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MappingException
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('deltaScenariosProvider')]
|
||||
public function testDeltaValidationOnUpdate(int $delta, string $errorMessage = ''): void
|
||||
{
|
||||
$point = new Point();
|
||||
|
||||
$point->setName('Edit point');
|
||||
$point->setDelta(5);
|
||||
$point->setType('form.submit');
|
||||
$point->setIsPublished(true);
|
||||
|
||||
$this->em->persist($point);
|
||||
$this->em->flush();
|
||||
|
||||
$pointId = $point->getId();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/points/edit/'.$pointId);
|
||||
$buttonCrawler = $crawler->selectButton('Save & Close');
|
||||
$form = $buttonCrawler->form();
|
||||
$form['point[name]']->setValue('Edit point');
|
||||
$this->testPointData($form, $delta, $errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array<mixed>>
|
||||
*/
|
||||
public static function deltaScenariosProvider(): iterable
|
||||
{
|
||||
yield 'within range positive number' => [3000, ''];
|
||||
yield 'within range negative number' => [-7857, ''];
|
||||
yield 'within range zero' => [0, ''];
|
||||
yield 'upper limit' => [IntHelper::MAX_INTEGER_VALUE, ''];
|
||||
yield 'lower limit' => [IntHelper::MIN_INTEGER_VALUE, ''];
|
||||
yield 'above upper limit' => [IntHelper::MAX_INTEGER_VALUE + 10, 'This value should be between -2147483648 and 2147483647.'];
|
||||
yield 'below lower limit' => [IntHelper::MIN_INTEGER_VALUE - 10, 'This value should be between -2147483648 and 2147483647.'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MappingException
|
||||
*/
|
||||
private function testPointData(Form $form, int $delta, string $errorMessage): void
|
||||
{
|
||||
$form['point[delta]']->setValue((string) $delta);
|
||||
$form['point[isPublished]']->setValue('1');
|
||||
$form['point[type]']->setValue('form.submit');
|
||||
|
||||
$this->client->submit($form);
|
||||
self::assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
$response = $this->client->getResponse()->getContent();
|
||||
self::assertStringContainsString($errorMessage, (string) $response);
|
||||
|
||||
$pointDetail = $this->em->getRepository(Point::class)->findOneBy(['delta' => $delta]);
|
||||
'' == $errorMessage ? self::assertNotNull($pointDetail) : self::assertNull($pointDetail);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Tests\Functional\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\PointBundle\Entity\Point;
|
||||
use Mautic\ProjectBundle\Entity\Project;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class PointControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testPointWithProject(): void
|
||||
{
|
||||
$point = new Point();
|
||||
$point->setName('test');
|
||||
$point->setType('url.hit');
|
||||
$this->em->persist($point);
|
||||
|
||||
$project = new Project();
|
||||
$project->setName('Test Project');
|
||||
$this->em->persist($project);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request('GET', '/s/points/edit/'.$point->getId());
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$form['point[projects]']->setValue((string) $project->getId());
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$savedAsset = $this->em->find(Point::class, $point->getId());
|
||||
Assert::assertSame($project->getId(), $savedAsset->getProjects()->first()->getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Tests\Functional\Controller;
|
||||
|
||||
use Mautic\PointBundle\Entity\Point;
|
||||
use Mautic\ProjectBundle\Tests\Functional\AbstractProjectSearchTestCase;
|
||||
|
||||
final class PointProjectSearchFunctionalTest extends AbstractProjectSearchTestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
|
||||
public function testProjectSearch(string $searchTerm, array $expectedEntities, array $unexpectedEntities): void
|
||||
{
|
||||
$projectOne = $this->createProject('Project One');
|
||||
$projectTwo = $this->createProject('Project Two');
|
||||
$projectThree = $this->createProject('Project Three');
|
||||
|
||||
$pointAlpha = $this->createPoint('Point Alpha');
|
||||
$pointBeta = $this->createPoint('Point Beta');
|
||||
$this->createPoint('Point Gamma');
|
||||
$this->createPoint('Point Delta');
|
||||
|
||||
$pointAlpha->addProject($projectOne);
|
||||
$pointAlpha->addProject($projectTwo);
|
||||
$pointBeta->addProject($projectTwo);
|
||||
$pointBeta->addProject($projectThree);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->searchAndAssert($searchTerm, $expectedEntities, $unexpectedEntities, ['/api/points', '/s/points']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator<string, array{searchTerm: string, expectedEntities: array<string>, unexpectedEntities: array<string>}>
|
||||
*/
|
||||
public static function searchDataProvider(): \Generator
|
||||
{
|
||||
yield 'search by one project' => [
|
||||
'searchTerm' => 'project:"Project Two"',
|
||||
'expectedEntities' => ['Point Alpha', 'Point Beta'],
|
||||
'unexpectedEntities' => ['Point Gamma', 'Point Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project AND point name' => [
|
||||
'searchTerm' => 'project:"Project Two" AND Beta',
|
||||
'expectedEntities' => ['Point Beta'],
|
||||
'unexpectedEntities' => ['Point Alpha', 'Point Gamma', 'Point Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project OR point name' => [
|
||||
'searchTerm' => 'project:"Project Two" OR Gamma',
|
||||
'expectedEntities' => ['Point Alpha', 'Point Beta', 'Point Gamma'],
|
||||
'unexpectedEntities' => ['Point Delta'],
|
||||
];
|
||||
|
||||
yield 'search by NOT one project' => [
|
||||
'searchTerm' => '!project:"Project Two"',
|
||||
'expectedEntities' => ['Point Gamma', 'Point Delta'],
|
||||
'unexpectedEntities' => ['Point Alpha', 'Point Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with AND' => [
|
||||
'searchTerm' => 'project:"Project Two" AND project:"Project Three"',
|
||||
'expectedEntities' => ['Point Beta'],
|
||||
'unexpectedEntities' => ['Point Alpha', 'Point Gamma', 'Point Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT AND' => [
|
||||
'searchTerm' => '!project:"Project Two" AND !project:"Project Three"',
|
||||
'expectedEntities' => ['Point Gamma', 'Point Delta'],
|
||||
'unexpectedEntities' => ['Point Alpha', 'Point Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with OR' => [
|
||||
'searchTerm' => 'project:"Project Two" OR project:"Project Three"',
|
||||
'expectedEntities' => ['Point Alpha', 'Point Beta'],
|
||||
'unexpectedEntities' => ['Point Gamma', 'Point Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT OR' => [
|
||||
'searchTerm' => '!project:"Project Two" OR !project:"Project Three"',
|
||||
'expectedEntities' => ['Point Alpha', 'Point Gamma', 'Point Delta'],
|
||||
'unexpectedEntities' => ['Point Beta'],
|
||||
];
|
||||
}
|
||||
|
||||
private function createPoint(string $name): Point
|
||||
{
|
||||
$point = new Point();
|
||||
$point->setName($name);
|
||||
$point->setType('url.hit');
|
||||
$this->em->persist($point);
|
||||
|
||||
return $point;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Tests\Functional\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\PointBundle\Entity\Trigger;
|
||||
use Mautic\ProjectBundle\Entity\Project;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class TriggerControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testPointTriggerWithProject(): void
|
||||
{
|
||||
$trigger = new Trigger();
|
||||
$trigger->setName('test');
|
||||
$this->em->persist($trigger);
|
||||
|
||||
$project = new Project();
|
||||
$project->setName('Test Project');
|
||||
$this->em->persist($project);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request('GET', '/s/points/triggers/edit/'.$trigger->getId());
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$form['pointtrigger[projects]']->setValue((string) $project->getId());
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$savedAsset = $this->em->find(Trigger::class, $trigger->getId());
|
||||
Assert::assertSame($project->getId(), $savedAsset->getProjects()->first()->getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Tests\Functional\Controller;
|
||||
|
||||
use Mautic\PointBundle\Entity\Trigger;
|
||||
use Mautic\ProjectBundle\Tests\Functional\AbstractProjectSearchTestCase;
|
||||
|
||||
final class TriggerProjectSearchFunctionalTest extends AbstractProjectSearchTestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
|
||||
public function testProjectSearch(string $searchTerm, array $expectedEntities, array $unexpectedEntities): void
|
||||
{
|
||||
$projectOne = $this->createProject('Project One');
|
||||
$projectTwo = $this->createProject('Project Two');
|
||||
$projectThree = $this->createProject('Project Three');
|
||||
|
||||
$triggerAlpha = $this->createTrigger('Trigger Alpha');
|
||||
$triggerBeta = $this->createTrigger('Trigger Beta');
|
||||
$this->createTrigger('Trigger Gamma');
|
||||
$this->createTrigger('Trigger Delta');
|
||||
|
||||
$triggerAlpha->addProject($projectOne);
|
||||
$triggerAlpha->addProject($projectTwo);
|
||||
$triggerBeta->addProject($projectTwo);
|
||||
$triggerBeta->addProject($projectThree);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->searchAndAssert($searchTerm, $expectedEntities, $unexpectedEntities, ['/api/points/triggers', '/s/points/triggers']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator<string, array{searchTerm: string, expectedEntities: array<string>, unexpectedEntities: array<string>}>
|
||||
*/
|
||||
public static function searchDataProvider(): \Generator
|
||||
{
|
||||
yield 'search by one project' => [
|
||||
'searchTerm' => 'project:"Project Two"',
|
||||
'expectedEntities' => ['Trigger Alpha', 'Trigger Beta'],
|
||||
'unexpectedEntities' => ['Trigger Gamma', 'Trigger Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project AND trigger name' => [
|
||||
'searchTerm' => 'project:"Project Two" AND Beta',
|
||||
'expectedEntities' => ['Trigger Beta'],
|
||||
'unexpectedEntities' => ['Trigger Alpha', 'Trigger Gamma', 'Trigger Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project OR trigger name' => [
|
||||
'searchTerm' => 'project:"Project Two" OR Gamma',
|
||||
'expectedEntities' => ['Trigger Alpha', 'Trigger Beta', 'Trigger Gamma'],
|
||||
'unexpectedEntities' => ['Trigger Delta'],
|
||||
];
|
||||
|
||||
yield 'search by NOT one project' => [
|
||||
'searchTerm' => '!project:"Project Two"',
|
||||
'expectedEntities' => ['Trigger Gamma', 'Trigger Delta'],
|
||||
'unexpectedEntities' => ['Trigger Alpha', 'Trigger Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with AND' => [
|
||||
'searchTerm' => 'project:"Project Two" AND project:"Project Three"',
|
||||
'expectedEntities' => ['Trigger Beta'],
|
||||
'unexpectedEntities' => ['Trigger Alpha', 'Trigger Gamma', 'Trigger Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT AND' => [
|
||||
'searchTerm' => '!project:"Project Two" AND !project:"Project Three"',
|
||||
'expectedEntities' => ['Trigger Gamma', 'Trigger Delta'],
|
||||
'unexpectedEntities' => ['Trigger Alpha', 'Trigger Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with OR' => [
|
||||
'searchTerm' => 'project:"Project Two" OR project:"Project Three"',
|
||||
'expectedEntities' => ['Trigger Alpha', 'Trigger Beta'],
|
||||
'unexpectedEntities' => ['Trigger Gamma', 'Trigger Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT OR' => [
|
||||
'searchTerm' => '!project:"Project Two" OR !project:"Project Three"',
|
||||
'expectedEntities' => ['Trigger Alpha', 'Trigger Gamma', 'Trigger Delta'],
|
||||
'unexpectedEntities' => ['Trigger Beta'],
|
||||
];
|
||||
}
|
||||
|
||||
private function createTrigger(string $name): Trigger
|
||||
{
|
||||
$trigger = new Trigger();
|
||||
$trigger->setName($name);
|
||||
$this->em->persist($trigger);
|
||||
|
||||
return $trigger;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Tests\Functional;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\PointBundle\Entity\Trigger;
|
||||
use Mautic\PointBundle\Entity\TriggerEvent;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\DomCrawler\Form;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class EmailTriggerTest extends MauticMysqlTestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\PreserveGlobalState(false)]
|
||||
#[\PHPUnit\Framework\Attributes\RunInSeparateProcess]
|
||||
public function testButtonsAreEnabledOnEditSendEmailToUserWhenEmailIsSelected(): void
|
||||
{
|
||||
$email = new Email();
|
||||
$email->setName('Some name');
|
||||
$email->setSubject('Some subject');
|
||||
$email->setTemplate('Blank');
|
||||
$email->setCustomHtml('Some html');
|
||||
$this->em->persist($email);
|
||||
$this->em->flush();
|
||||
|
||||
$trigger = $this->createTrigger();
|
||||
$triggerEvent = $this->createTriggerEvent($trigger);
|
||||
$triggerEvent->setProperties(['useremail' => ['email' => $email->getId()]]);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->detach($trigger);
|
||||
$this->em->detach($triggerEvent);
|
||||
|
||||
[$crawler, $form] = $this->fetchForm($trigger, $triggerEvent);
|
||||
|
||||
self::assertEquals($email->getId(), $form->get('pointtriggerevent[properties][useremail][email]')->getValue(), 'Current email should be selected.');
|
||||
self::assertNull($crawler->selectButton('Preview')->attr('disabled'), 'Preview button should not be disabled.');
|
||||
self::assertNull($crawler->selectButton('Edit Email')->attr('disabled'), 'Edit Email button should not be disabled.');
|
||||
self::assertStringContainsString('"origin":"#pointtriggerevent_properties_useremail_email"', $crawler->selectButton('Preview')->attr('onclick'), 'The origin value should be correct.');
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\PreserveGlobalState(false)]
|
||||
#[\PHPUnit\Framework\Attributes\RunInSeparateProcess]
|
||||
public function testButtonsAreDisabledWhenEmailIsNotSelected(): void
|
||||
{
|
||||
$trigger = $this->createTrigger();
|
||||
$triggerEvent = $this->createTriggerEvent($trigger);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->detach($trigger);
|
||||
$this->em->detach($triggerEvent);
|
||||
|
||||
[$crawler, $form] = $this->fetchForm($trigger, $triggerEvent);
|
||||
|
||||
self::assertEmpty($form->get('pointtriggerevent[properties][useremail][email]')->getValue(), 'No email should be selected.');
|
||||
self::assertNotNull($crawler->selectButton('Preview')->attr('disabled'), 'Preview button should be disabled.');
|
||||
self::assertNotNull($crawler->selectButton('Edit Email')->attr('disabled'), 'Edit Email button should be disabled.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{Crawler,Form}
|
||||
*/
|
||||
private function fetchForm(Trigger $trigger, TriggerEvent $triggerEvent): array
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/points/triggers/edit/'.$trigger->getId());
|
||||
self::assertTrue($this->client->getResponse()->isSuccessful());
|
||||
|
||||
$uri = sprintf('/s/points/triggers/events/edit/%s?triggerId=%s', $triggerEvent->getId(), $trigger->getId());
|
||||
$this->client->xmlHttpRequest(Request::METHOD_GET, $uri);
|
||||
self::assertTrue($this->client->getResponse()->isSuccessful());
|
||||
|
||||
$responseData = json_decode($this->client->getResponse()->getContent(), true);
|
||||
$crawler = new Crawler($responseData['newContent'], $this->client->getInternalRequest()->getUri());
|
||||
|
||||
$form = $crawler->filterXPath('//form[@name="pointtriggerevent"]')->form();
|
||||
|
||||
return [$crawler, $form];
|
||||
}
|
||||
|
||||
private function createTrigger(): Trigger
|
||||
{
|
||||
$trigger = new Trigger();
|
||||
$trigger->setName('Email Trigger');
|
||||
$this->em->persist($trigger);
|
||||
|
||||
return $trigger;
|
||||
}
|
||||
|
||||
private function createTriggerEvent(Trigger $trigger): TriggerEvent
|
||||
{
|
||||
$triggerEvent = new TriggerEvent();
|
||||
$triggerEvent->setTrigger($trigger);
|
||||
$triggerEvent->setName('Send email to user');
|
||||
$triggerEvent->setType('email.send_to_user');
|
||||
$triggerEvent->setProperties([]);
|
||||
$this->em->persist($triggerEvent);
|
||||
|
||||
return $triggerEvent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Tests\Functional;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\PointBundle\Entity\Group;
|
||||
use Mautic\PointBundle\Entity\GroupContactScore;
|
||||
use Mautic\PointBundle\Entity\GroupContactScoreRepository;
|
||||
|
||||
class GroupScoreRepositoryFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
protected GroupContactScoreRepository $repository;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->repository = $this->em->getRepository(GroupContactScore::class);
|
||||
}
|
||||
|
||||
public function testCompareScore(): void
|
||||
{
|
||||
$contact = $this->createContact('score@example.com');
|
||||
|
||||
$group = $this->createGroup('A');
|
||||
$this->addGroupContactScore($contact, $group, 7);
|
||||
$this->em->flush();
|
||||
|
||||
$this->assertTrue($this->repository->compareScore($contact->getId(), $group->getId(), 7, 'eq'));
|
||||
$this->assertFalse($this->repository->compareScore($contact->getId(), $group->getId(), 8, 'eq'));
|
||||
|
||||
$this->assertTrue($this->repository->compareScore($contact->getId(), $group->getId(), 8, 'neq'));
|
||||
$this->assertFalse($this->repository->compareScore($contact->getId(), $group->getId(), 7, 'neq'));
|
||||
|
||||
$this->assertTrue($this->repository->compareScore($contact->getId(), $group->getId(), 6, 'gt'));
|
||||
$this->assertFalse($this->repository->compareScore($contact->getId(), $group->getId(), 7, 'gt'));
|
||||
|
||||
$this->assertTrue($this->repository->compareScore($contact->getId(), $group->getId(), 8, 'lt'));
|
||||
$this->assertFalse($this->repository->compareScore($contact->getId(), $group->getId(), 7, 'lt'));
|
||||
|
||||
$this->assertTrue($this->repository->compareScore($contact->getId(), $group->getId(), 7, 'gte'));
|
||||
$this->assertTrue($this->repository->compareScore($contact->getId(), $group->getId(), 6, 'gte'));
|
||||
$this->assertFalse($this->repository->compareScore($contact->getId(), $group->getId(), 8, 'gte'));
|
||||
|
||||
$this->assertTrue($this->repository->compareScore($contact->getId(), $group->getId(), 7, 'lte'));
|
||||
$this->assertTrue($this->repository->compareScore($contact->getId(), $group->getId(), 8, 'lte'));
|
||||
$this->assertFalse($this->repository->compareScore($contact->getId(), $group->getId(), 6, 'lte'));
|
||||
}
|
||||
|
||||
public function testCompareScoreContactWithoutScoreInGroup(): void
|
||||
{
|
||||
$contactWithoutScore = $this->createContact('no-score@example.com');
|
||||
$group = $this->createGroup('A');
|
||||
$this->em->flush();
|
||||
|
||||
$this->assertFalse($this->repository->compareScore($contactWithoutScore->getId(), $group->getId(), 0, 'eq'));
|
||||
$this->assertFalse($this->repository->compareScore($contactWithoutScore->getId(), $group->getId(), 1, 'eq'));
|
||||
}
|
||||
|
||||
private function createContact(
|
||||
string $email,
|
||||
): Lead {
|
||||
$lead = new Lead();
|
||||
$lead->setEmail($email);
|
||||
$this->em->persist($lead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
private function createGroup(
|
||||
string $name,
|
||||
): Group {
|
||||
$group = new Group();
|
||||
$group->setName($name);
|
||||
$this->em->persist($group);
|
||||
|
||||
return $group;
|
||||
}
|
||||
|
||||
private function addGroupContactScore(
|
||||
Lead $lead,
|
||||
Group $group,
|
||||
int $score,
|
||||
): void {
|
||||
$groupContactScore = new GroupContactScore();
|
||||
$groupContactScore->setContact($lead);
|
||||
$groupContactScore->setGroup($group);
|
||||
$groupContactScore->setScore($score);
|
||||
$lead->addGroupScore($groupContactScore);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Tests\Functional;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Entity\Stat;
|
||||
use Mautic\EmailBundle\Entity\StatRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\PointBundle\Entity\Group;
|
||||
use Mautic\PointBundle\Entity\Point;
|
||||
|
||||
class PointActionFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testPointActionReadEmail(): void
|
||||
{
|
||||
$this->logoutUser();
|
||||
|
||||
/** @var LeadModel $leadModel */
|
||||
$leadModel = static::getContainer()->get('mautic.lead.model.lead');
|
||||
|
||||
$lead = $this->createLead('john@doe.email');
|
||||
$email = $this->createEmail();
|
||||
|
||||
$trackingHash = 'tracking_hash_123';
|
||||
$this->createEmailStat($lead, $email, $trackingHash);
|
||||
$pointAction = $this->createReadEmailAction(5);
|
||||
$this->client->request('GET', '/email/'.$trackingHash.'.gif');
|
||||
|
||||
$lead = $leadModel->getEntity($lead->getId());
|
||||
|
||||
$this->assertEquals($pointAction->getDelta(), $lead->getPoints());
|
||||
}
|
||||
|
||||
public function testPointActionWithGroupReadEmail(): void
|
||||
{
|
||||
$this->logoutUser();
|
||||
|
||||
/** @var LeadModel $leadModel */
|
||||
$leadModel = static::getContainer()->get('mautic.lead.model.lead');
|
||||
|
||||
$lead = $this->createLead('john@doe.email');
|
||||
$email = $this->createEmail();
|
||||
$group = $this->createGroup('Group A');
|
||||
|
||||
$trackingHash = 'tracking_hash_123';
|
||||
$this->createEmailStat($lead, $email, $trackingHash);
|
||||
$pointAction = $this->createReadEmailAction(5, $group);
|
||||
$this->client->request('GET', '/email/'.$trackingHash.'.gif');
|
||||
$this->em->clear(Lead::class);
|
||||
$lead = $leadModel->getEntity($lead->getId());
|
||||
$groupScore = $lead->getGroupScores()->first();
|
||||
|
||||
$this->assertEquals($pointAction->getDelta(), $groupScore->getScore());
|
||||
// group point action shouldn't update main contact points
|
||||
$this->assertEquals(0, $lead->getPoints());
|
||||
}
|
||||
|
||||
public function testPointActionEarlyReturnWhenNoPointsAvailable(): void
|
||||
{
|
||||
/** @var LeadModel $leadModel */
|
||||
$leadModel = static::getContainer()->get('mautic.lead.model.lead');
|
||||
|
||||
$lead = $this->createLead('jane@doe.email');
|
||||
$email = $this->createEmail();
|
||||
|
||||
$trackingHash = 'tracking_hash_no_points_456';
|
||||
$this->createEmailStat($lead, $email, $trackingHash);
|
||||
// Note: No point actions created for email.open type
|
||||
|
||||
$initialPoints = $lead->getPoints();
|
||||
$this->client->request('GET', '/email/'.$trackingHash.'.gif');
|
||||
|
||||
$lead = $leadModel->getEntity($lead->getId());
|
||||
|
||||
// Points should remain unchanged as no point actions are available
|
||||
$this->assertEquals($initialPoints, $lead->getPoints());
|
||||
$this->assertEquals(0, $lead->getPoints());
|
||||
}
|
||||
|
||||
private function createReadEmailAction(int $delta, ?Group $group = null): Point
|
||||
{
|
||||
$pointAction = new Point();
|
||||
$pointAction->setName('Read email action');
|
||||
$pointAction->setDelta($delta);
|
||||
$pointAction->setType('email.open');
|
||||
if ($group) {
|
||||
$pointAction->setGroup($group);
|
||||
}
|
||||
$this->em->persist($pointAction);
|
||||
$this->em->flush();
|
||||
|
||||
return $pointAction;
|
||||
}
|
||||
|
||||
private function createEmailStat(
|
||||
Lead $lead,
|
||||
Email $email,
|
||||
string $trackingHash,
|
||||
): Stat {
|
||||
/** @var StatRepository $statRepository */
|
||||
$statRepository = static::getContainer()->get('mautic.email.repository.stat');
|
||||
|
||||
$stat = new Stat();
|
||||
$stat->setTrackingHash($trackingHash);
|
||||
$stat->setEmailAddress($lead->getEmail());
|
||||
$stat->setLead($lead);
|
||||
$stat->setDateSent(new \DateTime());
|
||||
$stat->setEmail($email);
|
||||
$statRepository->saveEntity($stat);
|
||||
|
||||
return $stat;
|
||||
}
|
||||
|
||||
private function createLead(
|
||||
string $email,
|
||||
): Lead {
|
||||
$lead = new Lead();
|
||||
$lead->setEmail($email);
|
||||
$this->em->persist($lead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
private function createEmail(): Email
|
||||
{
|
||||
$email = new Email();
|
||||
$email->setName('Test email');
|
||||
$email->setSubject('Test email subject');
|
||||
$email->setEmailType('template');
|
||||
$email->setCustomHtml('<h1>Email content</h1><br>{signature}');
|
||||
$email->setIsPublished(true);
|
||||
$email->setFromAddress('from@api.test');
|
||||
$email->setFromName('API Test');
|
||||
$this->em->persist($email);
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
private function createGroup(
|
||||
string $name,
|
||||
): Group {
|
||||
$group = new Group();
|
||||
$group->setName($name);
|
||||
$this->em->persist($group);
|
||||
|
||||
return $group;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Tests\Functional;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\Tag;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\PointBundle\Entity\Group;
|
||||
use Mautic\PointBundle\Model\PointGroupModel;
|
||||
use Mautic\PointBundle\Model\TriggerModel;
|
||||
|
||||
class PointTriggerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
use TriggerTrait;
|
||||
|
||||
public function testPointsTriggerWithTagAction(): void
|
||||
{
|
||||
/** @var LeadModel $model */
|
||||
$model = self::getContainer()->get('mautic.lead.model.lead');
|
||||
|
||||
$trigger = $this->createTrigger('Trigger', 5);
|
||||
$this->createAddTagEvent('tag5', $trigger);
|
||||
$trigger = $this->createTrigger('Trigger', 6);
|
||||
$this->createAddTagEvent('tag6', $trigger);
|
||||
|
||||
$lead = new Lead();
|
||||
$data = ['email' => 'pointtest@example.com', 'points' => 5];
|
||||
$model->setFieldValues($lead, $data, false, true, true);
|
||||
$model->saveEntity($lead);
|
||||
|
||||
$this->em->clear(Lead::class);
|
||||
$lead = $model->getEntity($lead->getId());
|
||||
$this->assertFalse($lead->getTags()->isEmpty());
|
||||
$this->assertTrue($this->leadHasTag($lead, 'tag5'));
|
||||
$this->assertFalse($this->leadHasTag($lead, 'tag6'));
|
||||
}
|
||||
|
||||
public function testGroupPointsTriggerWithTagAction(): void
|
||||
{
|
||||
/** @var LeadModel $model */
|
||||
$model = self::getContainer()->get('mautic.lead.model.lead');
|
||||
|
||||
/** @var PointGroupModel $pointGroupModel */
|
||||
$pointGroupModel = self::getContainer()->get('mautic.point.model.group');
|
||||
|
||||
$groupA = $this->createGroup('Group A');
|
||||
$groupB = $this->createGroup('Group B');
|
||||
|
||||
$triggerA = $this->createTrigger('Group A Trigger (should trigger)', 5, $groupA);
|
||||
$this->createAddTagEvent('tagA', $triggerA);
|
||||
|
||||
$triggerB = $this->createTrigger('Group B Trigger (should not trigger)', 5, $groupB);
|
||||
$this->createAddTagEvent('tagB', $triggerB);
|
||||
|
||||
$lead = new Lead();
|
||||
$data = ['email' => 'pointtest@example.com', 'points' => 0];
|
||||
$model->setFieldValues($lead, $data, false, true, true);
|
||||
$model->saveEntity($lead);
|
||||
|
||||
$this->em->clear(Lead::class);
|
||||
$lead = $model->getEntity($lead->getId());
|
||||
$pointGroupModel->adjustPoints($lead, $groupA, 5);
|
||||
$lead = $model->getEntity($lead->getId());
|
||||
|
||||
$this->assertFalse($this->leadHasTag($lead, 'tagB'));
|
||||
$this->assertTrue($this->leadHasTag($lead, 'tagA'));
|
||||
}
|
||||
|
||||
public function testTriggerForExistingContacts(): void
|
||||
{
|
||||
/** @var LeadModel $leadModel */
|
||||
$leadModel = self::getContainer()->get('mautic.lead.model.lead');
|
||||
|
||||
/** @var TriggerModel $triggerModel */
|
||||
$triggerModel = self::getContainer()->get('mautic.point.model.trigger');
|
||||
|
||||
$lead = new Lead();
|
||||
$data = ['email' => 'pointtest@example.com', 'points' => 5];
|
||||
$leadModel->setFieldValues($lead, $data, false, true, true);
|
||||
$leadModel->saveEntity($lead);
|
||||
|
||||
$this->em->clear(Lead::class);
|
||||
|
||||
$triggerA = $this->createTrigger('Group A Trigger (should trigger)', 5, null, true);
|
||||
$triggerEventA = $this->createAddTagEvent('tagA', $triggerA);
|
||||
$triggerA->addTriggerEvent(0, $triggerEventA);
|
||||
$triggerModel->saveEntity($triggerA);
|
||||
|
||||
$triggerB = $this->createTrigger('Group B Trigger (should not trigger)', 6, null, true);
|
||||
$triggerEventB = $this->createAddTagEvent('tagB', $triggerB);
|
||||
$triggerB->addTriggerEvent(0, $triggerEventB);
|
||||
$triggerModel->saveEntity($triggerB);
|
||||
|
||||
$lead = $leadModel->getEntity($lead->getId());
|
||||
|
||||
$this->assertFalse($this->leadHasTag($lead, 'tagB'));
|
||||
$this->assertTrue($this->leadHasTag($lead, 'tagA'));
|
||||
}
|
||||
|
||||
public function testTriggerWithGroupForExistingContacts(): void
|
||||
{
|
||||
/** @var LeadModel $leadModel */
|
||||
$leadModel = self::getContainer()->get('mautic.lead.model.lead');
|
||||
|
||||
/** @var TriggerModel $triggerModel */
|
||||
$triggerModel = self::getContainer()->get('mautic.point.model.trigger');
|
||||
|
||||
/** @var PointGroupModel $pointGroupModel */
|
||||
$pointGroupModel = self::getContainer()->get('mautic.point.model.group');
|
||||
|
||||
$groupA = $this->createGroup('Group A');
|
||||
$groupB = $this->createGroup('Group B');
|
||||
|
||||
$lead = new Lead();
|
||||
$data = ['email' => 'pointtest@example.com'];
|
||||
$leadModel->setFieldValues($lead, $data, false, true, true);
|
||||
$leadModel->saveEntity($lead);
|
||||
$pointGroupModel->adjustPoints($lead, $groupA, 5);
|
||||
|
||||
$triggerA = $this->createTrigger('Group A Trigger (should trigger)', 5, $groupA, true);
|
||||
$triggerEventA = $this->createAddTagEvent('tagA', $triggerA);
|
||||
$triggerA->addTriggerEvent(0, $triggerEventA);
|
||||
$triggerModel->saveEntity($triggerA);
|
||||
|
||||
$triggerB = $this->createTrigger('Group B Trigger (should not trigger)', 5, $groupB, true);
|
||||
$triggerEventB = $this->createAddTagEvent('tagB', $triggerB);
|
||||
$triggerB->addTriggerEvent(0, $triggerEventB);
|
||||
$triggerModel->saveEntity($triggerB);
|
||||
$lead = $leadModel->getEntity($lead->getId());
|
||||
|
||||
$triggerC = $this->createTrigger('General Trigger (should not trigger)', 5, $groupB, true);
|
||||
$triggerEventB = $this->createAddTagEvent('tagC', $triggerC);
|
||||
$triggerC->addTriggerEvent(0, $triggerEventB);
|
||||
$triggerModel->saveEntity($triggerC);
|
||||
$lead = $leadModel->getEntity($lead->getId());
|
||||
|
||||
$this->assertFalse($this->leadHasTag($lead, 'tagC'));
|
||||
$this->assertFalse($this->leadHasTag($lead, 'tagB'));
|
||||
$this->assertTrue($this->leadHasTag($lead, 'tagA'));
|
||||
}
|
||||
|
||||
private function createGroup(
|
||||
string $name,
|
||||
): Group {
|
||||
$group = new Group();
|
||||
$group->setName($name);
|
||||
$this->em->persist($group);
|
||||
|
||||
return $group;
|
||||
}
|
||||
|
||||
private function leadHasTag(
|
||||
Lead $lead,
|
||||
string $tagName,
|
||||
): bool {
|
||||
/** @var Tag $tag */
|
||||
foreach ($lead->getTags() as $tag) {
|
||||
if ($tag->getTag() === $tagName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Tests\Functional;
|
||||
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\PointBundle\Entity\Group;
|
||||
use Mautic\PointBundle\Entity\GroupContactScore;
|
||||
use Mautic\ReportBundle\Entity\Report;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class ReportSubscriberFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->useCleanupRollback = false;
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testContactPointLogReportWithGroup(): void
|
||||
{
|
||||
$this->createTestContactWithGroupPoints();
|
||||
$report = new Report();
|
||||
$report->setName('Contact point log');
|
||||
$report->setSource('lead.pointlog');
|
||||
$report->setColumns(['lp.type', 'lp.event_name', 'l.email', 'lp.delta', 'pl.name']);
|
||||
$report->setTableOrder([[
|
||||
'column' => 'lp.delta',
|
||||
'direction' => 'DESC',
|
||||
]]);
|
||||
$this->em->persist($report);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
// -- test report table in mautic panel
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/s/reports/view/{$report->getId()}");
|
||||
$crawlerReportTable = $crawler->filterXPath('//table[@id="reportTable"]')->first();
|
||||
|
||||
// convert html table to php array
|
||||
$crawlerReportTable = $this->domTableToArray($crawlerReportTable);
|
||||
|
||||
$this->assertSame([
|
||||
// no., event_type, event_name, email, points_delta, group_name
|
||||
['1', 'test type', 'Adjust points', 'test2@example.com', '15', 'Group A'],
|
||||
['2', 'test type', 'Adjust points', 'test3@example.com', '10', 'Group A'],
|
||||
['3', 'test type', 'Adjust points', 'test1@example.com', '5', 'Group A'],
|
||||
['4', 'test type', 'Adjust points', 'test3@example.com', '2', 'Group B'],
|
||||
['5', 'test type', 'Adjust points', 'test2@example.com', '1', 'Group B'],
|
||||
], array_slice($crawlerReportTable, 1, 5));
|
||||
|
||||
// -- test API report data
|
||||
$this->client->request(Request::METHOD_GET, "/api/reports/{$report->getId()}");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$result = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertSame([
|
||||
[
|
||||
'type' => 'test type',
|
||||
'event_name' => 'Adjust points',
|
||||
'email' => 'test2@example.com',
|
||||
'delta' => '15',
|
||||
'group_name' => 'Group A',
|
||||
],
|
||||
[
|
||||
'type' => 'test type',
|
||||
'event_name' => 'Adjust points',
|
||||
'email' => 'test3@example.com',
|
||||
'delta' => '10',
|
||||
'group_name' => 'Group A',
|
||||
],
|
||||
[
|
||||
'type' => 'test type',
|
||||
'event_name' => 'Adjust points',
|
||||
'email' => 'test1@example.com',
|
||||
'delta' => '5',
|
||||
'group_name' => 'Group A',
|
||||
],
|
||||
[
|
||||
'type' => 'test type',
|
||||
'event_name' => 'Adjust points',
|
||||
'email' => 'test3@example.com',
|
||||
'delta' => '2',
|
||||
'group_name' => 'Group B',
|
||||
],
|
||||
[
|
||||
'type' => 'test type',
|
||||
'event_name' => 'Adjust points',
|
||||
'email' => 'test2@example.com',
|
||||
'delta' => '1',
|
||||
'group_name' => 'Group B',
|
||||
],
|
||||
], $result['data']);
|
||||
}
|
||||
|
||||
public function testGroupScoreReport(): void
|
||||
{
|
||||
$this->createTestContactWithGroupPoints();
|
||||
$report = new Report();
|
||||
$report->setName('Group score report');
|
||||
$report->setSource('group.score');
|
||||
$report->setColumns(['pl.name', 'ls.score', 'l.email']);
|
||||
$report->setTableOrder([[
|
||||
'column' => 'ls.score',
|
||||
'direction' => 'DESC',
|
||||
]]);
|
||||
$this->em->persist($report);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
// -- test report table in mautic panel
|
||||
$crawler = $this->client->request(Request::METHOD_GET, "/s/reports/view/{$report->getId()}");
|
||||
$crawlerReportTable = $crawler->filterXPath('//table[@id="reportTable"]')->first();
|
||||
|
||||
// convert html table to php array
|
||||
$crawlerReportTable = $this->domTableToArray($crawlerReportTable);
|
||||
|
||||
$this->assertSame([
|
||||
// no., group_name, group_score, email
|
||||
['1', 'Group A', '15', 'test2@example.com'],
|
||||
['2', 'Group A', '10', 'test3@example.com'],
|
||||
['3', 'Group A', '5', 'test1@example.com'],
|
||||
['4', 'Group B', '2', 'test3@example.com'],
|
||||
['5', 'Group B', '1', 'test2@example.com'],
|
||||
], array_slice($crawlerReportTable, 1, 5));
|
||||
|
||||
// -- test API report data
|
||||
$this->client->request(Request::METHOD_GET, "/api/reports/{$report->getId()}");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$result = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame([
|
||||
[
|
||||
'group_name' => 'Group A',
|
||||
'group_score' => '15',
|
||||
'email' => 'test2@example.com',
|
||||
],
|
||||
[
|
||||
'group_name' => 'Group A',
|
||||
'group_score' => '10',
|
||||
'email' => 'test3@example.com',
|
||||
],
|
||||
[
|
||||
'group_name' => 'Group A',
|
||||
'group_score' => '5',
|
||||
'email' => 'test1@example.com',
|
||||
],
|
||||
[
|
||||
'group_name' => 'Group B',
|
||||
'group_score' => '2',
|
||||
'email' => 'test3@example.com',
|
||||
],
|
||||
[
|
||||
'group_name' => 'Group B',
|
||||
'group_score' => '1',
|
||||
'email' => 'test2@example.com',
|
||||
],
|
||||
], $result['data']);
|
||||
}
|
||||
|
||||
private function createTestContactWithGroupPoints(): void
|
||||
{
|
||||
$contactModel = static::getContainer()->get('mautic.lead.model.lead');
|
||||
|
||||
$groupA = $this->createGroup('Group A');
|
||||
$groupB = $this->createGroup('Group B');
|
||||
$this->em->flush();
|
||||
|
||||
$contacts = [
|
||||
$this->createContact('test1@example.com'),
|
||||
$this->createContact('test2@example.com'),
|
||||
$this->createContact('test3@example.com'),
|
||||
];
|
||||
$contactModel->saveEntities($contacts);
|
||||
|
||||
$this->adjustContactPoints($contacts[0], 5, $groupA);
|
||||
$this->adjustContactPoints($contacts[1], 15, $groupA);
|
||||
$this->adjustContactPoints($contacts[2], 10, $groupA);
|
||||
$this->adjustContactPoints($contacts[2], 2, $groupB);
|
||||
$this->adjustContactPoints($contacts[1], 1, $groupB);
|
||||
|
||||
$contactModel->saveEntities($contacts);
|
||||
}
|
||||
|
||||
private function createContact(string $email): Lead
|
||||
{
|
||||
$contact = new Lead();
|
||||
$contact->setEmail($email);
|
||||
$this->em->persist($contact);
|
||||
|
||||
return $contact;
|
||||
}
|
||||
|
||||
private function adjustContactPoints(Lead $contact, int $points, Group $group): void
|
||||
{
|
||||
$ipAddress = new IpAddress();
|
||||
$ipAddress->setIpAddress('127.0.0.1');
|
||||
$contact->addPointsChangeLogEntry(
|
||||
'test type',
|
||||
'Adjust points',
|
||||
'test action',
|
||||
$points,
|
||||
$ipAddress,
|
||||
$group
|
||||
);
|
||||
$contact->adjustPoints($points);
|
||||
$groupContactScore = new GroupContactScore();
|
||||
$groupContactScore->setContact($contact);
|
||||
$groupContactScore->setGroup($group);
|
||||
$groupContactScore->setScore($points);
|
||||
$contact->addGroupScore($groupContactScore);
|
||||
}
|
||||
|
||||
private function createGroup(
|
||||
string $name,
|
||||
): Group {
|
||||
$group = new Group();
|
||||
$group->setName($name);
|
||||
$this->em->persist($group);
|
||||
|
||||
return $group;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,array<int,mixed>>
|
||||
*/
|
||||
private function domTableToArray(Crawler $crawler): array
|
||||
{
|
||||
return $crawler->filter('tr')->each(fn ($tr) => $tr->filter('td')->each(fn ($td) => trim($td->text())));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Tests\Functional;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\PointBundle\Entity\Group;
|
||||
use Mautic\PointBundle\Entity\GroupContactScore;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Component\Console\Tester\ApplicationTester;
|
||||
|
||||
class SegmentFilterFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
public function testGroupPointSegmentFilter(): void
|
||||
{
|
||||
$application = new Application(self::$kernel);
|
||||
$application->setAutoExit(false);
|
||||
$applicationTester = new ApplicationTester($application);
|
||||
|
||||
$contactA = $this->createContact('contact-a@example.com');
|
||||
$contactB = $this->createContact('contact-b@example.com');
|
||||
$contactC = $this->createContact('contact-c@example.com');
|
||||
$groupA = $this->createGroup('Group A');
|
||||
$this->em->flush();
|
||||
|
||||
$this->addGroupContactScore($contactA, $groupA, 1);
|
||||
$this->addGroupContactScore($contactB, $groupA, 0);
|
||||
$this->em->persist($contactA);
|
||||
$this->em->persist($contactB);
|
||||
$this->em->flush();
|
||||
|
||||
$segmentA = new LeadList();
|
||||
$segmentA->setName('Group A points >= 1');
|
||||
$segmentA->setPublicName('Group A points >= 1');
|
||||
$segmentA->setAlias('group-a-points-gte1');
|
||||
$segmentA->setIsPublished(true);
|
||||
$segmentA->setFilters([
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'group_points_'.$groupA->getId(),
|
||||
'object' => 'groups',
|
||||
'type' => 'number',
|
||||
'operator' => 'gte',
|
||||
'properties' => [
|
||||
'filter' => '1',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$this->em->persist($segmentA);
|
||||
$this->em->flush();
|
||||
|
||||
// Force Doctrine to re-fetch the entities otherwise the campaign won't know about any events.
|
||||
$this->em->clear();
|
||||
|
||||
// Execute segment update command.
|
||||
$exitCode = $applicationTester->run(
|
||||
[
|
||||
'command' => 'mautic:segments:update',
|
||||
'-i' => $segmentA->getId(),
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertSame(0, $exitCode, $applicationTester->getDisplay());
|
||||
|
||||
$this->client->request('GET', '/api/contacts?search=segment:group-a-points-gte1');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertTrue($this->client->getResponse()->isOk());
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertEquals(1, (int) $response['total']);
|
||||
$contactIds = array_column($response['contacts'], 'id');
|
||||
$this->assertContains((int) $contactA->getId(), $contactIds);
|
||||
$this->assertNotContains((int) $contactB->getId(), $contactIds);
|
||||
$this->assertNotContains((int) $contactC->getId(), $contactIds);
|
||||
}
|
||||
|
||||
private function createContact(
|
||||
string $email,
|
||||
): Lead {
|
||||
$lead = new Lead();
|
||||
$lead->setEmail($email);
|
||||
$this->em->persist($lead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
private function createGroup(
|
||||
string $name,
|
||||
): Group {
|
||||
$group = new Group();
|
||||
$group->setName($name);
|
||||
$this->em->persist($group);
|
||||
|
||||
return $group;
|
||||
}
|
||||
|
||||
private function addGroupContactScore(
|
||||
Lead $lead,
|
||||
Group $group,
|
||||
int $score,
|
||||
): void {
|
||||
$groupContactScore = new GroupContactScore();
|
||||
$groupContactScore->setContact($lead);
|
||||
$groupContactScore->setGroup($group);
|
||||
$groupContactScore->setScore($score);
|
||||
$lead->addGroupScore($groupContactScore);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PointBundle\Tests\Functional;
|
||||
|
||||
use Mautic\PointBundle\Entity\Group;
|
||||
use Mautic\PointBundle\Entity\Trigger;
|
||||
use Mautic\PointBundle\Entity\TriggerEvent;
|
||||
|
||||
trait TriggerTrait
|
||||
{
|
||||
private function createTrigger(
|
||||
string $name,
|
||||
int $points = 0,
|
||||
?Group $group = null,
|
||||
bool $triggerExistingLeads = false,
|
||||
): Trigger {
|
||||
$trigger = new Trigger();
|
||||
$trigger->setName($name);
|
||||
$trigger->setPoints($points);
|
||||
|
||||
if (isset($group)) {
|
||||
$trigger->setGroup($group);
|
||||
}
|
||||
if ($triggerExistingLeads) {
|
||||
$trigger->setTriggerExistingLeads($triggerExistingLeads);
|
||||
}
|
||||
$this->em->persist($trigger);
|
||||
|
||||
return $trigger;
|
||||
}
|
||||
|
||||
private function createAddTagEvent(
|
||||
string $tag,
|
||||
Trigger $trigger,
|
||||
): TriggerEvent {
|
||||
$triggerEvent = new TriggerEvent();
|
||||
$triggerEvent->setTrigger($trigger);
|
||||
$triggerEvent->setName('Add '.$tag);
|
||||
$triggerEvent->setType('lead.changetags');
|
||||
$triggerEvent->setProperties([
|
||||
'add_tags' => [$tag],
|
||||
'remove_tags' => [],
|
||||
]);
|
||||
$this->em->persist($triggerEvent);
|
||||
|
||||
return $triggerEvent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Tests\Unit\Helper;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\PointBundle\Helper\EventHelper;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class EventHelperTest extends TestCase
|
||||
{
|
||||
public function testEngagePointAction(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
|
||||
// Define the action array
|
||||
$action = ['id' => 1, 'type' => 'helloworld.action.custom_action', 'name' => 'My custom point action', 'properties' => [], 'points' => 50];
|
||||
|
||||
$points = EventHelper::engagePointAction($lead, $action);
|
||||
$this->assertEquals(50, $points);
|
||||
|
||||
$points = EventHelper::engagePointAction($lead, $action);
|
||||
$this->assertEquals(0, $points, 'Second call should return 0 points because the action is already initiated for this lead and type and session.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Tests\Unit\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\PageBundle\Entity\Hit;
|
||||
use Mautic\PageBundle\Helper\PointActionHelper;
|
||||
use Mautic\PointBundle\Entity\Point;
|
||||
use Mautic\PointBundle\Entity\PointRepository;
|
||||
use Mautic\PointBundle\Event\PointActionEvent;
|
||||
use Mautic\PointBundle\Event\PointBuilderEvent;
|
||||
use Mautic\PointBundle\Model\PointGroupModel;
|
||||
use Mautic\PointBundle\Model\PointModel;
|
||||
use Mautic\PointBundle\PointEvents;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class PointModelTest extends TestCase
|
||||
{
|
||||
private RequestStack&MockObject $requestStack;
|
||||
private IpLookupHelper&MockObject $ipLookupHelper;
|
||||
private LeadModel&MockObject $leadModel;
|
||||
private ContactTracker&MockObject $contactTracker;
|
||||
private EntityManager&MockObject $em;
|
||||
private CorePermissions&MockObject $security;
|
||||
private EventDispatcherInterface&MockObject $dispatcher;
|
||||
private UrlGeneratorInterface&MockObject $router;
|
||||
private Translator&MockObject $translator;
|
||||
private UserHelper&MockObject $userHelper;
|
||||
private LoggerInterface&MockObject $mauticLogger;
|
||||
private CoreParametersHelper&MockObject $coreParametersHelper;
|
||||
private PointGroupModel&MockObject $pointGroupModel;
|
||||
private PointModel $pointModel;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->requestStack = $this->createMock(RequestStack::class);
|
||||
$this->ipLookupHelper = $this->createMock(IpLookupHelper::class);
|
||||
$this->leadModel = $this->createMock(LeadModel::class);
|
||||
$this->contactTracker = $this->createMock(ContactTracker::class);
|
||||
$this->em = $this->createMock(EntityManager::class);
|
||||
$this->security = $this->createMock(CorePermissions::class);
|
||||
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$this->router = $this->createMock(RouterInterface::class);
|
||||
$this->translator = $this->createMock(Translator::class);
|
||||
$this->userHelper = $this->createMock(UserHelper::class);
|
||||
$this->mauticLogger = $this->createMock(LoggerInterface::class);
|
||||
$this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
$this->pointGroupModel = $this->createMock(PointGroupModel::class);
|
||||
$this->pointModel = new PointModel(
|
||||
$this->requestStack,
|
||||
$this->ipLookupHelper,
|
||||
$this->leadModel,
|
||||
$this->contactTracker,
|
||||
$this->em,
|
||||
$this->security,
|
||||
$this->dispatcher,
|
||||
$this->router,
|
||||
$this->translator,
|
||||
$this->userHelper,
|
||||
$this->mauticLogger,
|
||||
$this->coreParametersHelper,
|
||||
$this->pointGroupModel,
|
||||
);
|
||||
}
|
||||
|
||||
public function testTriggerUrlHitWithCallbackObject(): void
|
||||
{
|
||||
$type = 'url.hit';
|
||||
$pointId = 98783;
|
||||
$pointName = 'Point name';
|
||||
$pointProperties = ['property' => 'value'];
|
||||
$pointDelta = 7;
|
||||
$pointGroup = null;
|
||||
$ip = $this->createMock(IpAddress::class);
|
||||
$this->security->method('isAnonymous')->willReturn(true);
|
||||
$this->ipLookupHelper->method('getIpAddress')->willReturn($ip);
|
||||
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->expects(self::once())
|
||||
->method('adjustPoints')
|
||||
->with($pointDelta);
|
||||
$lead->expects(self::once())
|
||||
->method('addPointsChangeLogEntry')
|
||||
->with(
|
||||
'url',
|
||||
$pointId.': '.$pointName,
|
||||
'hit',
|
||||
$pointDelta,
|
||||
$ip,
|
||||
$pointGroup
|
||||
);
|
||||
$eventDetails = $this->createMock(Hit::class);
|
||||
|
||||
$repository = $this->createMock(PointRepository::class);
|
||||
$this->em->expects(self::once())
|
||||
->method('getRepository')
|
||||
->with(Point::class)
|
||||
->willReturn($repository);
|
||||
|
||||
$pointActionHelper = $this->createMock(PointActionHelper::class);
|
||||
$pointActionHelper->expects(self::once())
|
||||
->method('validateUrlHit')
|
||||
->with(
|
||||
$eventDetails,
|
||||
[
|
||||
'id' => $pointId,
|
||||
'type' => $type,
|
||||
'name' => $pointName,
|
||||
'properties' => $pointProperties,
|
||||
'points' => $pointDelta,
|
||||
]
|
||||
)
|
||||
->willReturn(true);
|
||||
|
||||
$point = $this->createMock(Point::class);
|
||||
$point->method('getRepeatable')->willReturn(true);
|
||||
$point->method('getType')->willReturn($type);
|
||||
$point->method('getId')->willReturn($pointId);
|
||||
$point->method('getName')->willReturn($pointName);
|
||||
$point->method('getProperties')->willReturn($pointProperties);
|
||||
$point->method('getDelta')->willReturn($pointDelta);
|
||||
$point->method('getGroup')->willReturn($pointGroup);
|
||||
|
||||
$repository->expects(self::once())
|
||||
->method('getPublishedByType')
|
||||
->with($type)
|
||||
->willReturn([$point]);
|
||||
$repository->expects(self::once())
|
||||
->method('getCompletedLeadActions')
|
||||
->willReturn([]);
|
||||
$repository->expects(self::never())
|
||||
->method('saveEntities');
|
||||
$repository->expects(self::never())
|
||||
->method('detachEntities');
|
||||
|
||||
$this->dispatcher->expects(self::exactly(2))
|
||||
->method('dispatch')
|
||||
->willReturnCallback(function (Event $event, string $eventName) use ($pointActionHelper, $type, $lead, $point): Event {
|
||||
if (PointEvents::POINT_ON_BUILD === $eventName) {
|
||||
self::assertInstanceOf(PointBuilderEvent::class, $event);
|
||||
self::assertEquals(new PointBuilderEvent($this->translator), $event);
|
||||
$event->addAction(
|
||||
$type,
|
||||
[
|
||||
'callback' => [
|
||||
$pointActionHelper,
|
||||
'validateUrlHit',
|
||||
],
|
||||
'group' => 'group',
|
||||
'label' => 'label',
|
||||
],
|
||||
);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
if (PointEvents::POINT_ON_ACTION === $eventName) {
|
||||
$pointActionEvent = new PointActionEvent($point, $lead);
|
||||
self::assertEquals($pointActionEvent, $event);
|
||||
|
||||
return $pointActionEvent;
|
||||
}
|
||||
|
||||
self::fail('Unknown event called: '.$eventName);
|
||||
});
|
||||
|
||||
$this->leadModel->expects(self::once())
|
||||
->method('saveEntity')
|
||||
->with($lead);
|
||||
|
||||
$this->pointModel->triggerAction($type, $eventDetails, null, $lead);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PointBundle\Tests\Unit\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\EmailBundle\EmailEvents;
|
||||
use Mautic\EmailBundle\Form\Type\EmailToUserType;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\PointBundle\Entity\TriggerEvent;
|
||||
use Mautic\PointBundle\Entity\TriggerEventRepository;
|
||||
use Mautic\PointBundle\Model\TriggerEventModel;
|
||||
use Mautic\PointBundle\Model\TriggerModel;
|
||||
use Mautic\PointBundle\PointEvents;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final class TriggerModelTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var IpLookupHelper&MockObject
|
||||
*/
|
||||
private MockObject $ipLookupHelper;
|
||||
|
||||
/**
|
||||
* @var LeadModel&MockObject
|
||||
*/
|
||||
private MockObject $leadModel;
|
||||
|
||||
/**
|
||||
* @var TriggerEventModel&MockObject
|
||||
*/
|
||||
private MockObject $triggerEventModel;
|
||||
|
||||
/**
|
||||
* @var EventDispatcherInterface&MockObject
|
||||
*/
|
||||
private MockObject $dispatcher;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface&MockObject
|
||||
*/
|
||||
private MockObject $translator;
|
||||
|
||||
/**
|
||||
* @var EntityManager&MockObject
|
||||
*/
|
||||
private MockObject $entityManager;
|
||||
|
||||
/**
|
||||
* @var TriggerEventRepository&MockObject
|
||||
*/
|
||||
private MockObject $triggerEventRepository;
|
||||
|
||||
private TriggerModel $triggerModel;
|
||||
|
||||
/**
|
||||
* @var ContactTracker&MockObject
|
||||
*/
|
||||
private MockObject $contactTracker;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->ipLookupHelper = $this->createMock(IpLookupHelper::class);
|
||||
$this->leadModel = $this->createMock(LeadModel::class);
|
||||
$this->triggerEventModel = $this->createMock(TriggerEventModel::class);
|
||||
$this->contactTracker = $this->createMock(ContactTracker::class);
|
||||
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$this->translator = $this->createMock(Translator::class);
|
||||
$this->entityManager = $this->createMock(EntityManager::class);
|
||||
$this->triggerEventRepository = $this->createMock(TriggerEventRepository::class);
|
||||
$this->triggerModel = new TriggerModel(
|
||||
$this->ipLookupHelper,
|
||||
$this->leadModel,
|
||||
$this->triggerEventModel,
|
||||
$this->contactTracker,
|
||||
$this->entityManager,
|
||||
$this->createMock(CorePermissions::class),
|
||||
$this->dispatcher,
|
||||
$this->createMock(UrlGeneratorInterface::class),
|
||||
$this->translator,
|
||||
$this->createMock(UserHelper::class),
|
||||
$this->createMock(LoggerInterface::class),
|
||||
$this->createMock(CoreParametersHelper::class)
|
||||
);
|
||||
|
||||
// reset private property cachedEvents in TriggerModel instance
|
||||
$reflectionClass = new \ReflectionClass(TriggerModel::class);
|
||||
$property = $reflectionClass->getProperty('cachedEvents');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($this->triggerModel, []);
|
||||
}
|
||||
|
||||
public function testTriggerEvent(): void
|
||||
{
|
||||
$triggerEvent = new TriggerEvent();
|
||||
$contact = new Lead();
|
||||
$dispatchCalls = new \ArrayObject();
|
||||
|
||||
$triggerEvent->setType('email.send_to_user');
|
||||
|
||||
$this->entityManager->expects($this->once())
|
||||
->method('getRepository')
|
||||
->willReturn($this->triggerEventRepository);
|
||||
|
||||
$this->triggerEventRepository->expects($this->once())
|
||||
->method('find')
|
||||
->willReturn($triggerEvent);
|
||||
|
||||
$this->dispatcher->expects($this->exactly(2))
|
||||
->method('dispatch')
|
||||
->willReturnCallback(function ($event, $eventName) use ($dispatchCalls, $contact, $triggerEvent) {
|
||||
$dispatchCalls->append($eventName);
|
||||
|
||||
if (PointEvents::TRIGGER_ON_BUILD === $eventName) {
|
||||
// Emulate a subscriber:
|
||||
$event->addEvent(
|
||||
'email.send_to_user',
|
||||
[
|
||||
'group' => 'mautic.email.point.trigger',
|
||||
'label' => 'mautic.email.point.trigger.send_email_to_user',
|
||||
'formType' => EmailToUserType::class,
|
||||
'formTypeOptions' => ['update_select' => 'pointtriggerevent_properties_useremail_email'],
|
||||
'formTheme' => 'MauticEmailBundle:FormTheme\EmailSendList',
|
||||
'eventName' => EmailEvents::ON_SENT_EMAIL_TO_USER,
|
||||
]
|
||||
);
|
||||
|
||||
return $event;
|
||||
} elseif (EmailEvents::ON_SENT_EMAIL_TO_USER === $eventName) {
|
||||
Assert::assertSame($contact, $event->getLead());
|
||||
Assert::assertSame($triggerEvent, $event->getTriggerEvent());
|
||||
|
||||
return $event;
|
||||
} else {
|
||||
$this->fail("Unexpected event name: $eventName");
|
||||
}
|
||||
});
|
||||
|
||||
$this->triggerModel->triggerEvent($triggerEvent->convertToArray(), $contact, true);
|
||||
|
||||
// Assert both expected events were dispatched
|
||||
Assert::assertContains(PointEvents::TRIGGER_ON_BUILD, $dispatchCalls);
|
||||
Assert::assertContains(EmailEvents::ON_SENT_EMAIL_TO_USER, $dispatchCalls);
|
||||
Assert::assertCount(2, $dispatchCalls);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
mautic.point.error.notfound="No point action with an id of %id% was found!"
|
||||
mautic.point.notice.batch_deleted="%count% point actions have been deleted!"
|
||||
mautic.point.trigger.error.notfound="No trigger with an id of %id% was found!"
|
||||
mautic.point.trigger.notice.batch_deleted="%count% point triggers have been deleted!"
|
||||
@@ -0,0 +1,91 @@
|
||||
mautic.point.point="Point"
|
||||
mautic.point.action.delta="Change points (+/-)"
|
||||
mautic.point.action.delta.help="Set the number of points to be added or deducted for this action. If it is a positive number, it will be added to the contact's points. If negative, it will be deducted."
|
||||
mautic.point.action.noresults.tip="Use point actions to adjust a contact's point score based on defined events. For example, give a contact 10 points if he/she opens a campaign email."
|
||||
mautic.point.actions.header.index="Point Actions"
|
||||
mautic.point.event.gained="Point gained"
|
||||
mautic.point.event.manual_change="Manual change"
|
||||
mautic.point.form.score_not_set="Score not set"
|
||||
mautic.point.form.no_point_groups="There are no point groups available."
|
||||
mautic.point.form.addaction="Use the list to the right to add an action."
|
||||
mautic.point.form.confirmbatchdelete="Delete the selected point actions?"
|
||||
mautic.point.form.confirmdelete="Delete the point action, %name%?"
|
||||
mautic.point.form.repeat="Is repeatable"
|
||||
mautic.protip.points.decay="Implement a lead scoring decay model to accurately capture intent."
|
||||
mautic.protip.points.reduce="Reduce scores systematically over time if contacts become inactive."
|
||||
mautic.protip.points.limit="Use a limit to prevent scores from getting too high or low."
|
||||
mautic.protip.triggers.behavioral="Use behavioral triggers to send timely, relevant emails based on user actions."
|
||||
mautic.point.form.type="Action taken by contact"
|
||||
mautic.point.form.repeat.help="Enable to score leads multiple times for this action; disable for one-time scoring."
|
||||
mautic.point.form.repeat.yes="Allow multiple scores"
|
||||
mautic.point.form.repeat.no="One-time scoring only"
|
||||
mautic.point.menu.edit="Edit Point Action"
|
||||
mautic.point.menu.index="Manage Actions"
|
||||
mautic.point.menu.new="New Point Action"
|
||||
mautic.point.permissions.header="Point Permissions"
|
||||
mautic.point.permissions.points="Point Actions - User has access to"
|
||||
mautic.point.permissions.triggers="Triggers - User has access to"
|
||||
mautic.point.permissions.groups="Groups - User has access to"
|
||||
mautic.point.thead.action="Action"
|
||||
mautic.point.thead.delta="Points +/-"
|
||||
mautic.point.thead.group="Group"
|
||||
mautic.point.trigger.addevent="Select an event from the 'Add an event' list."
|
||||
mautic.point.trigger.event.add="Add an event"
|
||||
mautic.point.trigger.form.color="Contact color"
|
||||
mautic.point.trigger.form.color_descr="Contacts with at least the number of points above will be designated this color."
|
||||
mautic.point.trigger.form.confirmbatchdelete="Delete the selected triggers?"
|
||||
mautic.point.trigger.form.confirmdelete="Delete the trigger, %name%?"
|
||||
mautic.point.trigger.form.existingleads="Trigger for existing applicable contacts upon saving (if published)?"
|
||||
mautic.point.trigger.form.modalheader="Trigger Action Details"
|
||||
mautic.point.trigger.form.points="Minimum number of points"
|
||||
mautic.point.trigger.form.points_descr="Minimum number of points required in order to trigger associated actions."
|
||||
mautic.point.trigger.header.edit="Edit Trigger"
|
||||
mautic.point.trigger.header.index="Point Triggers"
|
||||
mautic.point.trigger.header.new="New Trigger"
|
||||
mautic.point.trigger.menu.edit="Edit Trigger Event"
|
||||
mautic.point.trigger.menu.index="Manage Triggers"
|
||||
mautic.point.trigger.noresults.tip="Create a trigger to take some action with the contact once a certain point level is reached. You can adjust a contact's campaign, push to a CRM, send an email, etc. Define a color to easily see where your contacts are at a glance when viewing contact lists!"
|
||||
mautic.point.trigger.tab.events="Events"
|
||||
mautic.point.trigger.thead.points="Point Total"
|
||||
mautic.point.trigger.thead.pointstrigger="Trigger at Points"
|
||||
mautic.point.group.menu.index="Manage Groups"
|
||||
mautic.point.group.menu.new="New Group"
|
||||
mautic.point.group.menu.edit="Edit Group"
|
||||
mautic.point.group.header.index="Point Groups"
|
||||
mautic.point.group.form.group="Point group"
|
||||
mautic.point.group.form.group_descr="Choose the point group to which this applies. If no groups are selected, it will apply to the main contact points."
|
||||
mautic.point.group.form.nogroup="None"
|
||||
mautic.point.group.form.confirmdelete="Delete the point group, %name%?"
|
||||
mautic.points.menu.root="Points"
|
||||
mautic.point.action.empty.heading="Unlock automated lead scoring with Actions"
|
||||
mautic.point.action.empty.subheading="Stop guessing which leads are engaged! Focus your follow-up efforts purely on closing deals."
|
||||
mautic.point.action.empty.understand="Understand"
|
||||
mautic.point.action.empty.what_are_point_actions="What are point actions?"
|
||||
mautic.point.action.empty.what_are_point_actions_desc="These are rules you create to automatically adjust contact scores. You control the point value (positive or negative) for each action, tailoring the system to your business."
|
||||
mautic.point.action.empty.purpose="Purpose"
|
||||
mautic.point.action.empty.score_contacts_heading="Score contacts based on specific behaviors"
|
||||
mautic.point.action.empty.score_contacts_desc="Assign points automatically when contacts download an asset, visit your pricing page, open a crucial email, or submit a key form."
|
||||
mautic.point.trigger.empty.heading="Trigger automations when engagement peaks"
|
||||
mautic.point.trigger.empty.subheading="Automatically respond when a contact's engagement level, measured by points, hits a key threshold."
|
||||
mautic.point.dashboard.widgets="Point Widgets"
|
||||
mautic.widget.points.in.time="Points in time"
|
||||
mautic.point.changes="Point changes"
|
||||
mautic.point.group.report.table="Group score"
|
||||
mautic.point.report.group_id="Group ID"
|
||||
mautic.point.report.group_name="Group name"
|
||||
mautic.point.report.group_score="Group score"
|
||||
mautic.point.trigger.onboarding.eyebrow="Understand"
|
||||
mautic.point.trigger.onboarding.heading="How point triggers work?"
|
||||
mautic.point.trigger.onboarding.copy="Triggers execute predefined actions – like changing a segment, adding a tag, or pushing to your CRM – the moment a contact reaches a specific score."
|
||||
mautic.point.trigger.onboarding.step1="Create a new trigger and set the minimum number of points a contact must reach."
|
||||
mautic.point.trigger.onboarding.step2="Select one or more events to execute when the point threshold is met."
|
||||
mautic.point.trigger.onboarding.step3="Publish the trigger. You can choose whether to apply it immediately to existing contacts who already meet the criteria."
|
||||
mautic.point.trigger.onboarding.notification.title="Triggers act differently of campaign-based events"
|
||||
mautic.point.trigger.onboarding.notification.content="They activate whenever the score threshold is met, ensuring immediate action based on engagement level."
|
||||
mautic.point.trigger.onboarding.group.heading="Setting up score-triggered events"
|
||||
mautic.point.trigger.onboarding.cta="Create new trigger"
|
||||
mautic.point.trigger.onboarding.usecase.eyebrow="Use case"
|
||||
mautic.point.trigger.onboarding.usecase.heading="Applying practically"
|
||||
mautic.point.group.onboarding.heading="Gain deeper insight with granular lead scoring"
|
||||
mautic.point.group.onboarding.subheading="Go beyond a single engagement score. Groups allow you to categorize points based on different types of interactions or interests."
|
||||
mautic.point.group.onboarding.copy="Track separate scores for engagement with specific product lines, content topics, or funnel stages. This gives you a much richer understanding of what your contacts are interested in, not just how engaged they are overall."
|
||||
@@ -0,0 +1,3 @@
|
||||
mautic.point.form.actions.notempty="At least one action is required."
|
||||
mautic.point.type.notblank="A point action must be selected."
|
||||
mautic.point.delta.notblank="A point change (delta) must be selected."
|
||||
Reference in New Issue
Block a user