Merge branch 'main' of github.com:cerebrate-project/cerebrate into main

pull/54/head
mokaddem 2021-06-01 15:21:13 +02:00
commit 88752c8b16
45 changed files with 1654 additions and 114 deletions

View File

@ -0,0 +1,108 @@
<?php
use Cake\ORM\TableRegistry;
require_once(ROOT . DS . 'libraries' . DS . 'default' . DS . 'RequestProcessors' . DS . 'GenericRequestProcessor.php');
class BroodRequestProcessor extends GenericRequestProcessor
{
protected $scope = 'Brood';
protected $action = 'not-specified'; //overriden when extending
protected $description = ''; // overriden when extending
protected $registeredActions = [
'ToolInterconnection',
'OneWaySynchronization',
];
public function __construct($loadFromAction=false) {
parent::__construct($loadFromAction);
}
public function create($requestData)
{
return parent::create($requestData);
}
}
class ToolInterconnectionProcessor extends BroodRequestProcessor implements GenericProcessorActionI {
public $action = 'ToolInterconnection';
protected $description;
public function __construct() {
parent::__construct();
$this->description = __('Handle tool interconnection request from other cerebrate instance');
$this->Broods = TableRegistry::getTableLocator()->get('Broods');
}
protected function addValidatorRules($validator)
{
return $validator;
}
public function create($requestData) {
$this->validateRequestData($requestData);
$requestData['title'] = __('Cerebrate instance {0} requested interconnection for tool {1}', 'Insert brood name', 'Insert tool name');
return parent::create($requestData);
}
public function process($id, $requestData)
{
$connectionSuccessfull = false;
$interConnectionResult = [];
if ($connectionSuccessfull) {
$this->discard($id, $requestData);
}
return $this->genActionResult(
$interConnectionResult,
$connectionSuccessfull,
$connectionSuccessfull ? __('Interconnection for `{0}` created', 'Insert tool name') : __('Could interconnect tool `{0}`.', 'Insert tool name'),
[]
);
}
public function discard($id, $requestData)
{
return parent::discard($id, $requestData);
}
}
class OneWaySynchronizationProcessor extends BroodRequestProcessor implements GenericProcessorActionI {
public $action = 'OneWaySynchronization';
protected $description;
public function __construct() {
parent::__construct();
$this->description = __('Handle cerebrate connection request for another cerebrate instance');
$this->Broods = TableRegistry::getTableLocator()->get('Broods');
}
protected function addValidatorRules($validator)
{
return $validator;
}
public function create($requestData) {
$this->validateRequestData($requestData);
$requestData['title'] = __('Cerebrate instance {0} requested interconnection', 'Insert cerebrate name');
return parent::create($requestData);
}
public function process($id, $requestData)
{
$connectionSuccessfull = false;
$interConnectionResult = [];
if ($connectionSuccessfull) {
$this->discard($id, $requestData);
}
return $this->genActionResult(
$interConnectionResult,
$connectionSuccessfull,
$connectionSuccessfull ? __('Interconnection with `{0}` created', 'Insert cerebrate name') : __('Could interconnect with `{0}`.', 'Insert cerebrate name'),
[]
);
}
public function discard($id, $requestData)
{
return parent::discard($id, $requestData);
}
}

View File

@ -0,0 +1,202 @@
<?php
use Cake\ORM\TableRegistry;
use Cake\Filesystem\File;
use Cake\Utility\Inflector;
use Cake\Validation\Validator;
use Cake\View\ViewBuilder;
interface GenericProcessorActionI
{
public function create($requestData);
public function process($requestID, $serverRequest);
public function discard($requestID ,$requestData);
}
class GenericRequestProcessor
{
protected $Inbox;
protected $registeredActions = [];
protected $validator;
private $processingTemplate = '/genericTemplates/confirm';
private $processingTemplatesDirectory = ROOT . '/libraries/default/RequestProcessors/templates';
public function __construct($registerActions=false) {
$this->Inbox = TableRegistry::getTableLocator()->get('Inbox');
if ($registerActions) {
$this->registerActionInProcessor();
}
$processingTemplatePath = $this->getProcessingTemplatePath();
$file = new File($this->processingTemplatesDirectory . DS . $processingTemplatePath);
if ($file->exists()) {
$this->processingTemplate = str_replace('.php', '', $processingTemplatePath);
}
$file->close();
}
public function getRegisteredActions()
{
return $this->registeredActions;
}
public function getScope()
{
return $this->scope;
}
private function getProcessingTemplatePath()
{
$class = str_replace('RequestProcessor', '', get_parent_class($this));
$action = strtolower(str_replace('Processor', '', get_class($this)));
return sprintf('%s/%s.php',
$class,
$action
);
}
public function getProcessingTemplate()
{
return $this->processingTemplate;
}
public function render($request=[])
{
$processingTemplate = $this->getProcessingTemplate();
$viewVariables = $this->getViewVariables($request);
$builder = new ViewBuilder();
$builder->disableAutoLayout()
->setClassName('Monad')
->setTemplate($processingTemplate);
$view = $builder->build($viewVariables);
return $view->render();
}
protected function generateRequest($requestData)
{
$request = $this->Inbox->newEmptyEntity();
$request = $this->Inbox->patchEntity($request, $requestData);
if ($request->getErrors()) {
throw new Exception(__('Could not create request.{0}Reason: {1}', PHP_EOL, json_encode($request->getErrors())), 1);
}
return $request;
}
protected function validateRequestData($requestData)
{
$errors = [];
if (!isset($requestData['data'])) {
$errors[] = __('No request data provided');
}
$validator = new Validator();
if (method_exists($this, 'addValidatorRules')) {
$validator = $this->addValidatorRules($validator);
$errors = $validator->validate($requestData['data']);
}
if (!empty($errors)) {
throw new Exception('Error while validating request data. ' . json_encode($errors), 1);
}
}
protected function registerActionInProcessor()
{
foreach ($this->registeredActions as $i => $action) {
$className = "{$action}Processor";
$reflection = new ReflectionClass($className);
if ($reflection->isAbstract() || $reflection->isInterface()) {
throw new Exception(__('Cannot create instance of %s, as it is abstract or is an interface'));
}
$this->{$action} = $reflection->newInstance();
}
}
protected function getViewVariablesConfirmModal($id, $title='', $question='', $actionName='')
{
return [
'title' => !empty($title) ? $title : __('Process request {0}', $id),
'question' => !empty($question) ? $question : __('Confirm request {0}', $id),
'actionName' => !empty($actionName) ? $actionName : __('Confirm'),
'path' => ['controller' => 'inbox', 'action' => 'process', $id]
];
}
public function getViewVariables($request)
{
return $this->getViewVariablesConfirmModal($request->id, '', '', '');
}
protected function genActionResult($data, $success, $message, $errors=[])
{
return [
'data' => $data,
'success' => $success,
'message' => $message,
'errors' => $errors,
];
}
public function genHTTPReply($controller, $processResult, $redirect=null)
{
$scope = $this->scope;
$action = $this->action;
if ($processResult['success']) {
$message = !empty($processResult['message']) ? $processResult['message'] : __('Request {0} successfully processed.', $id);
if ($controller->ParamHandler->isRest()) {
$response = $controller->RestResponse->viewData($processResult, 'json');
} else if ($controller->ParamHandler->isAjax()) {
$response = $controller->RestResponse->ajaxSuccessResponse('RequestProcessor', "{$scope}.{$action}", $processResult['data'], $message);
} else {
$controller->Flash->success($message);
if (!is_null($redirect)) {
$response = $controller->redirect($redirect);
} else {
$response = $controller->redirect(['action' => 'index']);
}
}
} else {
$message = !empty($processResult['message']) ? $processResult['message'] : __('Request {0} could not be processed.', $id);
if ($controller->ParamHandler->isRest()) {
$response = $controller->RestResponse->viewData($processResult, 'json');
} else if ($controller->ParamHandler->isAjax()) {
$response = $controller->RestResponse->ajaxFailResponse('RequestProcessor', "{$scope}.{$action}", $processResult['data'], $message, $processResult['errors']);
} else {
$controller->Flash->error($message);
if (!is_null($redirect)) {
$response = $controller->redirect($redirect);
} else {
$response = $controller->redirect(['action' => 'index']);
}
}
}
return $response;
}
public function checkLoading()
{
return 'Assimilation successful!';
}
public function create($requestData)
{
$requestData['scope'] = $this->scope;
$requestData['action'] = $this->action;
$requestData['description'] = $this->description;
$request = $this->generateRequest($requestData);
$savedRequest = $this->Inbox->save($request);
return $this->genActionResult(
$savedRequest,
$savedRequest !== false,
__('{0} request for {1} created', $this->scope, $this->action),
$request->getErrors()
);
}
public function discard($id, $requestData)
{
$request = $this->Inbox->get($id);
$this->Inbox->delete($request);
return $this->genActionResult(
[],
true,
__('{0}.{1} request #{2} discarded', $this->scope, $this->action, $id)
);
}
}

View File

@ -0,0 +1,65 @@
<?php
use Cake\ORM\TableRegistry;
require_once(ROOT . DS . 'libraries' . DS . 'default' . DS . 'RequestProcessors' . DS . 'GenericRequestProcessor.php');
class ProposalRequestProcessor extends GenericRequestProcessor
{
protected $scope = 'Proposal';
protected $action = 'not-specified'; //overriden when extending
protected $description = ''; // overriden when extending
protected $registeredActions = [
'ProposalEdit'
];
public function __construct($loadFromAction=false) {
parent::__construct($loadFromAction);
}
public function create($requestData)
{
return parent::create($requestData);
}
}
class ProposalEditProcessor extends ProposalRequestProcessor implements GenericProcessorActionI {
public $action = 'ProposalEdit';
protected $description;
public function __construct() {
parent::__construct();
$this->description = __('Handle proposal from users for this cerebrate instance');
$this->Users = TableRegistry::getTableLocator()->get('Users');
}
protected function addValidatorRules($validator)
{
return $validator;
}
public function create($requestData) {
$this->validateRequestData($requestData);
$requestData['title'] = __('User `{0}` would like to modify record `{0}`', 'username', 'recordname');
return parent::create($requestData);
}
public function process($id, $requestData)
{
$proposalAccepted = false;
$saveResult = [];
if ($proposalAccepted) {
$this->discard($id, $requestData);
}
return $this->genActionResult(
$saveResult,
$proposalAccepted,
$proposalAccepted ? __('Record `{0}` modify', 'recordname') : __('Could modify record `{0}`.', 'recordname'),
[]
);
}
public function discard($id, $requestData)
{
return parent::discard($id, $requestData);
}
}

View File

@ -0,0 +1,65 @@
<?php
use Cake\ORM\TableRegistry;
require_once(ROOT . DS . 'libraries' . DS . 'default' . DS . 'RequestProcessors' . DS . 'GenericRequestProcessor.php');
class SynchronisationRequestProcessor extends GenericRequestProcessor
{
protected $scope = 'Synchronisation';
protected $action = 'not-specified'; //overriden when extending
protected $description = ''; // overriden when extending
protected $registeredActions = [
'DataExchange'
];
public function __construct($loadFromAction=false) {
parent::__construct($loadFromAction);
}
public function create($requestData)
{
return parent::create($requestData);
}
}
class DataExchangeProcessor extends SynchronisationRequestProcessor implements GenericProcessorActionI {
public $action = 'DataExchange';
protected $description;
public function __construct() {
parent::__construct();
$this->description = __('Handle exchange of data between two cerebrate instances');
$this->Users = TableRegistry::getTableLocator()->get('Users');
}
protected function addValidatorRules($validator)
{
return $validator;
}
public function create($requestData) {
$this->validateRequestData($requestData);
$requestData['title'] = __('Data exchange requested for record `{0}`', 'recordname');
return parent::create($requestData);
}
public function process($id, $requestData)
{
$dataExchangeAccepted = false;
$saveResult = [];
if ($dataExchangeAccepted) {
$this->discard($id, $requestData);
}
return $this->genActionResult(
$saveResult,
$dataExchangeAccepted,
$dataExchangeAccepted ? __('Record `{0}` exchanged', 'recordname') : __('Could not exchange record `{0}`.', 'recordname'),
[]
);
}
public function discard($id, $requestData)
{
return parent::discard($id, $requestData);
}
}

View File

@ -0,0 +1,65 @@
<?php
use Cake\ORM\TableRegistry;
require_once(ROOT . DS . 'libraries' . DS . 'default' . DS . 'RequestProcessors' . DS . 'GenericRequestProcessor.php');
class SCOPE_RequestProcessor extends GenericRequestProcessor
{
protected $scope = '~to-be-defined~';
protected $action = 'not-specified'; //overriden when extending
protected $description = ''; // overriden when extending
protected $registeredActions = [
'ACTION'
];
public function __construct($loadFromAction=false) {
parent::__construct($loadFromAction);
}
public function create($requestData)
{
return parent::create($requestData);
}
}
class SCOPE_ACTION_Processor extends ProposalRequestProcessor implements GenericProcessorActionI {
public $action = 'ACTION';
protected $description;
public function __construct() {
parent::__construct();
$this->description = __('~to-be-defined~');
$this->Users = TableRegistry::getTableLocator()->get('Users');
}
protected function addValidatorRules($validator)
{
return $validator;
}
public function create($requestData) {
$this->validateRequestData($requestData);
$requestData['title'] = __('~to-be-defined~');
return parent::create($requestData);
}
public function process($id, $requestData)
{
$proposalAccepted = false;
$saveResult = [];
if ($proposalAccepted) {
$this->discard($id, $requestData);
}
return $this->genActionResult(
$saveResult,
$proposalAccepted,
$proposalAccepted ? __('success') : __('fail'),
[]
);
}
public function discard($id, $requestData)
{
return parent::discard($id, $requestData);
}
}

View File

@ -0,0 +1,121 @@
<?php
use Cake\ORM\TableRegistry;
require_once(ROOT . DS . 'libraries' . DS . 'default' . DS . 'RequestProcessors' . DS . 'GenericRequestProcessor.php');
class UserRequestProcessor extends GenericRequestProcessor
{
protected $scope = 'User';
protected $action = 'not-specified'; //overriden when extending
protected $description = ''; // overriden when extending
protected $registeredActions = [
'Registration'
];
protected $Users;
public function __construct($loadFromAction=false) {
parent::__construct($loadFromAction);
$this->Users = TableRegistry::getTableLocator()->get('Users');
}
public function create($requestData)
{
return parent::create($requestData);
}
}
class RegistrationProcessor extends UserRequestProcessor implements GenericProcessorActionI {
public $action = 'Registration';
protected $description;
public function __construct() {
parent::__construct();
$this->description = __('Handle user account for this cerebrate instance');
}
protected function addValidatorRules($validator)
{
return $validator
->notEmpty('username', 'A username must be provided.')
->add('email', 'validFormat', [
'rule' => 'email',
'message' => 'E-mail must be valid'
])
->notEmpty('first_name', 'A first name must be provided')
->notEmpty('last_name', 'A last name must be provided');
}
public function create($requestData) {
$this->validateRequestData($requestData);
$requestData['title'] = __('User account creation requested for {0}', $requestData['data']['email']);
return parent::create($requestData);
}
public function getViewVariables($request)
{
$dropdownData = [
'role' => $this->Users->Roles->find('list', [
'sort' => ['name' => 'asc']
]),
'individual' => [-1 => __('-- New individual --')] + $this->Users->Individuals->find('list', [
'sort' => ['email' => 'asc']
])->toArray()
];
$individualEntity = $this->Users->Individuals->newEntity([
'email' => !empty($request['data']['email']) ? $request['data']['email'] : '',
'first_name' => !empty($request['data']['first_name']) ? $request['data']['first_name'] : '',
'last_name' => !empty($request['data']['last_name']) ? $request['data']['last_name'] : '',
'position' => !empty($request['data']['position']) ? $request['data']['position'] : '',
]);
$userEntity = $this->Users->newEntity([
'individual_id' => -1,
'username' => !empty($request['data']['username']) ? $request['data']['username'] : '',
'role_id' => !empty($request['data']['role_id']) ? $request['data']['role_id'] : '',
'disabled' => !empty($request['data']['disabled']) ? $request['data']['disabled'] : '',
]);
return [
'dropdownData' => $dropdownData,
'userEntity' => $userEntity,
'individualEntity' => $individualEntity
];
}
public function process($id, $requestData)
{
if ($requestData['individual_id'] == -1) {
$individual = $this->Users->Individuals->newEntity([
'uuid' => $requestData['uuid'],
'email' => $requestData['email'],
'first_name' => $requestData['first_name'],
'last_name' => $requestData['last_name'],
'position' => $requestData['position'],
]);
$individual = $this->Users->Individuals->save($individual);
} else {
$individual = $this->Users->Individuals->get($requestData['individual_id']);
}
$user = $this->Users->newEntity([
'individual_id' => $individual->id,
'username' => $requestData['username'],
'password' => '~PASSWORD_TO_BE_REPLACED~',
'role_id' => $requestData['role_id'],
'disabled' => $requestData['disabled'],
]);
$user = $this->Users->save($user);
if ($user !== false) {
$this->discard($id, $requestData);
}
return $this->genActionResult(
$user,
$user !== false,
$user !== false ? __('User `{0}` created', $user->username) : __('Could not create user `{0}`.', $user->username),
$user->getErrors()
);
}
public function discard($id, $requestData)
{
return parent::discard($id, $requestData);
}
}

View File

@ -0,0 +1,125 @@
<?php
$formUser = $this->element('genericElements/Form/genericForm', [
'entity' => $userEntity,
'ajax' => false,
'raw' => true,
'data' => [
'description' => __('Create user account'),
'model' => 'User',
'fields' => [
[
'field' => 'individual_id',
'type' => 'dropdown',
'label' => __('Associated individual'),
'options' => $dropdownData['individual'],
],
[
'field' => 'username',
'autocomplete' => 'off',
],
[
'field' => 'role_id',
'type' => 'dropdown',
'label' => __('Role'),
'options' => $dropdownData['role']
],
[
'field' => 'disabled',
'type' => 'checkbox',
'label' => 'Disable'
]
],
'submit' => [
'action' => $this->request->getParam('action')
]
]
]);
$formIndividual = $this->element('genericElements/Form/genericForm', [
'entity' => $individualEntity,
'ajax' => false,
'raw' => true,
'data' => [
'description' => __('Create individual'),
'model' => 'Individual',
'fields' => [
[
'field' => 'email',
'autocomplete' => 'off'
],
[
'field' => 'uuid',
'label' => 'UUID',
'type' => 'uuid',
'autocomplete' => 'off'
],
[
'field' => 'first_name',
'autocomplete' => 'off'
],
[
'field' => 'last_name',
'autocomplete' => 'off'
],
[
'field' => 'position',
'autocomplete' => 'off'
],
],
'submit' => [
'action' => $this->request->getParam('action')
]
]
]);
echo $this->Bootstrap->modal([
'title' => __('Register user'),
'size' => 'lg',
'type' => 'confirm',
'bodyHtml' => sprintf('<div class="user-container">%s</div><div class="individual-container">%s</div>',
$formUser,
$formIndividual
),
'confirmText' => __('Create user'),
'confirmFunction' => 'submitRegistration'
]);
?>
</div>
<script>
function submitRegistration(modalObject, tmpApi) {
const $forms = modalObject.$modal.find('form')
const url = $forms[0].action
const data1 = getFormData($forms[0])
const data2 = getFormData($forms[1])
const data = {...data1, ...data2}
return tmpApi.postData(url, data)
}
$(document).ready(function() {
$('div.user-container #individual_id-field').change(function() {
if ($(this).val() == -1) {
$('div.individual-container').show()
} else {
$('div.individual-container').hide()
}
})
})
function getFormData(form) {
return Object.values(form).reduce((obj,field) => {
if (field.type === 'checkbox') {
obj[field.name] = field.checked;
} else {
obj[field.name] = field.value;
}
return obj
}, {})
}
</script>
<style>
div.individual-container > div, div.user-container > div {
font-size: 1.5rem;
}
</style>

View File

@ -107,6 +107,12 @@ class AppController extends Controller
} else if ($this->ParamHandler->isRest()) {
throw new MethodNotAllowedException(__('Invalid user credentials.'));
}
if ($this->request->getParam('action') === 'index') {
$this->Security->setConfig('validatePost', false);
}
$this->Security->setConfig('unlockedActions', ['index']);
$this->ACL->checkAccess();
$this->set('menu', $this->ACL->getMenu());
$this->set('ajax', $this->request->is('ajax'));

View File

@ -6,6 +6,7 @@ use App\Controller\AppController;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use \Cake\Database\Expression\QueryExpression;
use Cake\ORM\TableRegistry;
class BroodsController extends AppController
{
@ -154,4 +155,22 @@ class BroodsController extends AppController
$this->redirect($this->referer());
}
}
public function interconnectTools()
{
$this->requestProcessor = TableRegistry::getTableLocator()->get('RequestProcessor');
$processor = $this->requestProcessor->getProcessor('Brood', 'ToolInterconnection');
$data = [
'origin' => '127.0.0.1',
'comment' => 'Test comment',
'user_id' => $this->ACL->getUser()->id,
'data' => [
'foo' => 'foo',
'bar' => 'bar',
'baz' => 'baz',
],
];
$processorResult = $processor->create($data);
return $processor->genHTTPReply($this, $processorResult, ['controller' => 'Broods', 'action' => 'index']);
}
}

View File

@ -642,6 +642,29 @@ class ACLComponent extends Component
]
]
],
'Inbox' => [
'label' => __('Inbox'),
'url' => '/inbox/index',
'children' => [
'index' => [
'url' => '/inbox/index',
'label' => __('Inbox')
],
'view' => [
'url' => '/inbox/view/{{id}}',
'label' => __('View Meta Template'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1
],
'delete' => [
'url' => '/inbox/delete/{{id}}',
'label' => __('Delete Meta Template'),
'actions' => ['delete', 'edit', 'view'],
'skipTopMenu' => 1,
'popup' => 1
]
]
],
'MetaTemplates' => [
'label' => __('Meta Field Templates'),
'url' => '/metaTemplates/index',

View File

@ -165,7 +165,7 @@ class CRUDComponent extends Component
$message = __(
'{0} could not be added.{1}',
$this->ObjectAlias,
empty($validationMessage) ? '' : ' ' . __('Reason:{0}', $validationMessage)
empty($validationMessage) ? '' : PHP_EOL . __('Reason:{0}', $validationMessage)
);
if ($this->Controller->ParamHandler->isRest()) {
} else if ($this->Controller->ParamHandler->isAjax()) {
@ -508,7 +508,7 @@ class CRUDComponent extends Component
if (is_bool($contextFromField)) {
$contextFromFieldText = sprintf('%s: %s', $field, $contextFromField ? 'true' : 'false');
} else {
$contextFromFieldText = $contextFromField;
$contextFromFieldText = sprintf('%s: %s', $field, $contextFromField);
}
$filteringContexts[] = [
'label' => Inflector::humanize($contextFromFieldText),

View File

@ -423,11 +423,12 @@ class RestResponseComponent extends Component
public function ajaxSuccessResponse($ObjectAlias, $action, $entity, $message, $additionalData=[])
{
$action = $this->__dissectAdminRouting($action);
$entity = is_array($entity) ? $entity : $entity->toArray();
$response = [
'success' => true,
'message' => $message,
'data' => $entity->toArray()
//'url' => $this->__generateURL($action, $ObjectAlias, $entity->id)
'data' => $entity,
'url' => !empty($entity['id']) ? $this->__generateURL($action, $ObjectAlias, $entity['id']) : ''
];
if (!empty($additionalData)) {
$response['additionalData'] = $additionalData;
@ -438,11 +439,12 @@ class RestResponseComponent extends Component
public function ajaxFailResponse($ObjectAlias, $action, $entity, $message, $errors = [])
{
$action = $this->__dissectAdminRouting($action);
$entity = is_array($entity) ? $entity : $entity->toArray();
$response = [
'success' => false,
'message' => $message,
'errors' => $errors,
'url' => $this->__generateURL($action, $ObjectAlias, $entity->id)
'url' => !empty($entity['id']) ? $this->__generateURL($action, $ObjectAlias, $entity['id']) : ''
];
return $this->viewData($response);
}

View File

@ -18,7 +18,7 @@ class EncryptionKeysController extends AppController
{
$this->CRUD->index([
'quickFilters' => ['encryption_key'],
'filters' => ['owner_type', 'organisation_id', 'individual_id', 'encryption_key'],
'filters' => ['owner_model', 'organisation_id', 'individual_id', 'encryption_key'],
'contextFilters' => [
'fields' => [
'type'

View File

@ -0,0 +1,131 @@
<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\Database\Expression\QueryExpression;
use Cake\Event\EventInterface;
use Cake\ORM\TableRegistry;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use Cake\Http\Exception\NotFoundException;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Exception\ForbiddenException;
class InboxController extends AppController
{
public $filters = ['scope', 'action', 'title', 'origin', 'comment'];
public function beforeFilter(EventInterface $event)
{
parent::beforeFilter($event);
$this->set('metaGroup', 'Administration');
}
public function index()
{
$this->CRUD->index([
'filters' => $this->filters,
'quickFilters' => ['scope', 'action', ['title' => true], ['comment' => true]],
'contextFilters' => [
'fields' => [
'scope',
'action',
]
],
'contain' => ['Users']
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
public function filtering()
{
$this->CRUD->filtering();
}
public function view($id)
{
$this->CRUD->view($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
public function delete($id)
{
if ($this->request->is('post')) {
$request = $this->Inbox->get($id);
$this->requestProcessor = TableRegistry::getTableLocator()->get('RequestProcessor');
$processor = $this->requestProcessor->getProcessor($request->scope, $request->action);
$discardResult = $processor->discard($id, $request);
return $processor->genHTTPReply($this, $discardResult);
}
$this->set('deletionTitle', __('Discard request'));
$this->set('deletionText', __('Are you sure you want to discard request #{0}?', $id));
$this->set('deletionConfirm', __('Discard'));
$this->CRUD->delete($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
}
public function process($id)
{
$request = $this->Inbox->get($id);
$scope = $request->scope;
$action = $request->action;
$this->requestProcessor = TableRegistry::getTableLocator()->get('RequestProcessor');
$processor = $this->requestProcessor->getProcessor($request->scope, $request->action);
if ($this->request->is('post')) {
$processResult = $processor->process($id, $this->request->getData());
return $processor->genHTTPReply($this, $processResult);
} else {
$renderedView = $processor->render($request);
return $this->response->withStringBody($renderedView);
}
}
public function listProcessors()
{
$this->requestProcessor = TableRegistry::getTableLocator()->get('RequestProcessor');
$requestProcessors = $this->requestProcessor->listProcessors();
if ($this->ParamHandler->isRest()) {
return $this->RestResponse->viewData($requestProcessors, 'json');
}
$data = [];
foreach ($requestProcessors as $scope => $processors) {
foreach ($processors as $processor) {
$data[] = [
'enabled' => $processor->enabled,
'scope' => $scope,
'action' => $processor->action
];
}
}
$this->set('title', 'Available request processors');
$this->set('fields', [
[
'name' => 'Enabled',
'data_path' => 'enabled',
'element' => 'boolean'
],
[
'name' => 'Processor scope',
'data_path' => 'scope',
],
[
'name' => 'Processor action',
'data_path' => 'action',
]
]);
$this->set('data', $data);
$this->render('/genericTemplates/index_simple');
}
}

View File

@ -4,6 +4,7 @@ namespace App\Controller;
use App\Controller\AppController;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use Cake\ORM\TableRegistry;
use \Cake\Database\Expression\QueryExpression;
class UsersController extends AppController
@ -134,4 +135,22 @@ class UsersController extends AppController
return $this->redirect(\Cake\Routing\Router::url('/users/login'));
}
}
public function register()
{
$this->requestProcessor = TableRegistry::getTableLocator()->get('RequestProcessor');
$processor = $this->requestProcessor->getProcessor('User', 'Registration');
$data = [
'origin' => '127.0.0.1',
'comment' => 'Hi there!, please create an account',
'data' => [
'username' => 'foobar',
'email' => 'foobar@admin.test',
'first_name' => 'foo',
'last_name' => 'bar',
],
];
$processorResult = $processor->create($data);
return $processor->genHTTPReply($this, $processorResult, ['controller' => 'Inbox', 'action' => 'index']);
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
use Cake\ORM\Entity;
class Inbox extends AppModel
{
}

View File

@ -18,14 +18,14 @@ class EncryptionKeysTable extends AppTable
'Individuals',
[
'foreignKey' => 'owner_id',
'conditions' => ['owner_type' => 'individual']
'conditions' => ['owner_model' => 'individual']
]
);
$this->belongsTo(
'Organisations',
[
'foreignKey' => 'owner_id',
'conditions' => ['owner_type' => 'organisation']
'conditions' => ['owner_model' => 'organisation']
]
);
$this->setDisplayField('encryption_key');
@ -34,13 +34,13 @@ class EncryptionKeysTable extends AppTable
public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)
{
if (empty($data['owner_id'])) {
if (empty($data['owner_type'])) {
if (empty($data['owner_model'])) {
return false;
}
if (empty($data[$data['owner_type'] . '_id'])) {
if (empty($data[$data['owner_model'] . '_id'])) {
return false;
}
$data['owner_id'] = $data[$data['owner_type'] . '_id'];
$data['owner_id'] = $data[$data['owner_model'] . '_id'];
}
}
@ -50,8 +50,8 @@ class EncryptionKeysTable extends AppTable
->notEmptyString('type')
->notEmptyString('encryption_key')
->notEmptyString('owner_id')
->notEmptyString('owner_type')
->requirePresence(['type', 'encryption_key', 'owner_id', 'owner_type'], 'create');
->notEmptyString('owner_model')
->requirePresence(['type', 'encryption_key', 'owner_id', 'owner_model'], 'create');
return $validator;
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\Database\Schema\TableSchemaInterface;
use Cake\Database\Type;
use Cake\ORM\Table;
use Cake\ORM\RulesChecker;
use Cake\Validation\Validator;
Type::map('json', 'Cake\Database\Type\JsonType');
class InboxTable extends AppTable
{
public function initialize(array $config): void
{
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('Timestamp', [
'events' => [
'Model.beforeSave' => [
'created' => 'new'
]
]
]);
$this->belongsTo('Users');
$this->setDisplayField('title');
}
protected function _initializeSchema(TableSchemaInterface $schema): TableSchemaInterface
{
$schema->setColumnType('data', 'json');
return $schema;
}
public function validationDefault(Validator $validator): Validator
{
$validator
->notEmptyString('scope')
->notEmptyString('action')
->notEmptyString('title')
->notEmptyString('origin')
->datetime('created')
->requirePresence([
'scope' => ['message' => __('The field `scope` is required')],
'action' => ['message' => __('The field `action` is required')],
'title' => ['message' => __('The field `title` is required')],
'origin' => ['message' => __('The field `origin` is required')],
], 'create');
return $validator;
}
public function buildRules(RulesChecker $rules): RulesChecker
{
$rules->add($rules->existsIn('user_id', 'Users'), [
'message' => 'The provided `user_id` does not exist'
]);
return $rules;
}
}

View File

@ -25,7 +25,7 @@ class IndividualsTable extends AppTable
'EncryptionKeys',
[
'foreignKey' => 'owner_id',
'conditions' => ['owner_type' => 'individual']
'conditions' => ['owner_model' => 'individual']
]
);
$this->hasOne(

View File

@ -30,7 +30,7 @@ class OrganisationsTable extends AppTable
[
'dependent' => true,
'foreignKey' => 'owner_id',
'conditions' => ['owner_type' => 'organisation']
'conditions' => ['owner_model' => 'organisation']
]
);
$this->hasMany(

View File

@ -0,0 +1,95 @@
<?php
namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\Filesystem\Folder;
class RequestProcessorTable extends AppTable
{
private $processorsDirectory = ROOT . '/libraries/default/RequestProcessors';
private $requestProcessors;
private $enabledProcessors = [ // to be defined in config
'Brood' => [
'ToolInterconnection' => false,
'OneWaySynchronization' => false,
],
'Proposal' => [
'ProposalEdit' => false,
],
'Synchronisation' => [
'DataExchange' => false,
],
'User' => [
'Registration' => true,
],
];
public function initialize(array $config): void
{
parent::initialize($config);
$this->loadProcessors();
}
public function getProcessor($scope, $action=null)
{
if (isset($this->requestProcessors[$scope])) {
if (is_null($action)) {
return $this->requestProcessors[$scope];
} else if (!empty($this->requestProcessors[$scope]->{$action})) {
return $this->requestProcessors[$scope]->{$action};
} else {
throw new \Exception(__('Processor {0}.{1} not found', $scope, $action));
}
}
throw new \Exception(__('Processor not found'), 1);
}
public function listProcessors($scope=null)
{
if (is_null($scope)) {
return $this->requestProcessors;
} else {
if (isset($this->requestProcessors[$scope])) {
return $this->requestProcessors[$scope];
} else {
throw new \Exception(__('Processors for {0} not found', $scope));
}
}
}
private function loadProcessors()
{
$processorDir = new Folder($this->processorsDirectory);
$processorFiles = $processorDir->find('.*RequestProcessor\.php', true);
foreach ($processorFiles as $processorFile) {
if ($processorFile == 'GenericRequestProcessor.php') {
continue;
}
$processorMainClassName = str_replace('.php', '', $processorFile);
$processorMainClassNameShort = str_replace('RequestProcessor.php', '', $processorFile);
$processorMainClass = $this->getProcessorClass($processorDir->pwd() . DS . $processorFile, $processorMainClassName);
if ($processorMainClass !== false) {
$this->requestProcessors[$processorMainClassNameShort] = $processorMainClass;
foreach ($this->requestProcessors[$processorMainClassNameShort]->getRegisteredActions() as $registeredAction) {
$scope = $this->requestProcessors[$processorMainClassNameShort]->getScope();
if (!empty($this->enabledProcessors[$scope][$registeredAction])) {
$this->requestProcessors[$processorMainClassNameShort]->{$registeredAction}->enabled = true;
} else {
$this->requestProcessors[$processorMainClassNameShort]->{$registeredAction}->enabled = false;
}
}
}
}
}
private function getProcessorClass($filePath, $processorMainClassName)
{
require_once($filePath);
$reflection = new \ReflectionClass($processorMainClassName);
$processorMainClass = $reflection->newInstance(true);
if ($processorMainClass->checkLoading() === 'Assimilation successful!') {
return $processorMainClass;
}
}
}

View File

@ -721,7 +721,7 @@ class BoostrapModal extends BootstrapGeneric {
'footerClass' => [''],
'type' => 'ok-only',
'variant' => '',
'confirmFunction' => '',
'confirmFunction' => '', // Will be called with the following arguments confirmFunction(modalObject, tmpApi)
'cancelFunction' => ''
];
@ -822,7 +822,7 @@ class BoostrapModal extends BootstrapGeneric {
'variant' => 'primary',
'text' => __('Ok'),
'params' => [
'data-dismiss' => 'modal',
'data-dismiss' => $this->options['confirmFunction'] ? '' : 'modal',
'onclick' => $this->options['confirmFunction']
]
]))->button();
@ -847,9 +847,10 @@ class BoostrapModal extends BootstrapGeneric {
$buttonConfirm = (new BoostrapButton([
'variant' => $variant,
'text' => h($this->options['confirmText']),
'class' => 'modal-confirm-button',
'params' => [
'data-dismiss' => 'modal',
'onclick' => $this->options['confirmFunction']
'data-dismiss' => $this->options['confirmFunction'] ? '' : 'modal',
'data-confirmFunction' => sprintf('%s', $this->options['confirmFunction'])
]
]))->button();
return $buttonCancel . $buttonConfirm;

34
src/View/MonadView.php Normal file
View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.4
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace App\View;
/**
* A view class that supports rendering view file belonging to directories outside of the main application template folder.
*/
class MonadView extends AppView
{
private $additionalTemplatePaths = [
ROOT . '/libraries/default/RequestProcessors/templates/',
];
protected function _paths(?string $plugin = null, bool $cached = true): array
{
$paths = parent::_paths($plugin, $cached);
$paths = array_merge($paths, $this->additionalTemplatePaths);
return $paths;
}
}

View File

@ -6,7 +6,7 @@ echo $this->element('genericElements/Form/genericForm', [
'model' => 'Organisations',
'fields' => [
[
'field' => 'owner_type',
'field' => 'owner_model',
'label' => __('Owner type'),
'options' => array_combine(array_keys($dropdownData), array_keys($dropdownData)),
'type' => 'dropdown'
@ -17,7 +17,7 @@ echo $this->element('genericElements/Form/genericForm', [
'options' => $dropdownData['organisation'] ?? [],
'type' => 'dropdown',
'stateDependence' => [
'source' => '#owner_type-field',
'source' => '#owner_model-field',
'option' => 'organisation'
]
],
@ -27,7 +27,7 @@ echo $this->element('genericElements/Form/genericForm', [
'options' => $dropdownData['individual'] ?? [],
'type' => 'dropdown',
'stateDependence' => [
'source' => '#owner_type-field',
'source' => '#owner_model-field',
'option' => 'individual'
]
],

View File

@ -45,7 +45,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
[
'name' => __('Owner'),
'data_path' => 'owner_id',
'owner_type_path' => 'owner_type',
'owner_model_path' => 'owner_model',
'element' => 'owner'
],
[

99
templates/Inbox/index.php Normal file
View File

@ -0,0 +1,99 @@
<?php
echo $this->Html->scriptBlock(sprintf(
'var csrfToken = %s;',
json_encode($this->request->getAttribute('csrfToken'))
));
echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'data' => $data,
'top_bar' => [
'children' => [
[
'type' => 'context_filters',
'context_filters' => !empty($filteringContexts) ? $filteringContexts : []
],
[
'type' => 'search',
'button' => __('Filter'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value',
'allowFilering' => true
]
]
],
'fields' => [
[
'name' => '#',
'sort' => 'id',
'data_path' => 'id',
],
[
'name' => 'created',
'sort' => 'created',
'data_path' => 'created',
'element' => 'datetime'
],
[
'name' => 'scope',
'sort' => 'scope',
'data_path' => 'scope',
],
[
'name' => 'action',
'sort' => 'action',
'data_path' => 'action',
],
[
'name' => 'title',
'sort' => 'title',
'data_path' => 'title',
],
[
'name' => 'origin',
'sort' => 'origin',
'data_path' => 'origin',
],
[
'name' => 'user',
'sort' => 'user_id',
'data_path' => 'user',
'element' => 'user'
],
[
'name' => 'description',
'sort' => 'description',
'data_path' => 'description',
],
[
'name' => 'comment',
'sort' => 'comment',
'data_path' => 'comment',
],
],
'title' => __('Inbox'),
'description' => __('A list of requests to be manually processed'),
'actions' => [
[
'url' => '/inbox/view',
'url_params_data_paths' => ['id'],
'icon' => 'eye',
'title' => __('View request')
],
[
'open_modal' => '/inbox/process/[onclick_params_data_path]',
'modal_params_data_path' => 'id',
'icon' => 'cogs',
'title' => __('Process request')
],
[
'open_modal' => '/inbox/delete/[onclick_params_data_path]',
'modal_params_data_path' => 'id',
'icon' => 'trash',
'title' => __('Discard request')
],
]
]
]);
echo '</div>';
?>

51
templates/Inbox/view.php Normal file
View File

@ -0,0 +1,51 @@
<?php
echo $this->element(
'/genericElements/SingleViews/single_view',
[
'data' => $entity,
'fields' => [
[
'key' => __('ID'),
'path' => 'id'
],
[
'key' => 'created',
'path' => 'created',
],
[
'key' => 'scope',
'path' => 'scope',
],
[
'key' => 'action',
'path' => 'action',
],
[
'key' => 'title',
'path' => 'title',
],
[
'key' => 'origin',
'path' => 'origin',
],
[
'key' => 'user_id',
'path' => 'user_id',
],
[
'key' => 'description',
'path' => 'description',
],
[
'key' => 'comment',
'path' => 'comment',
],
[
'key' => 'data',
'path' => 'data',
'type' => 'json'
],
],
'children' => []
]
);

View File

@ -51,7 +51,7 @@ function runAllUpdate() {
type: 'confirm-success',
confirmText: '<?= __n('Run update', 'Run all updates', count($updateAvailables)) ?>',
APIConfirm: (tmpApi) => {
tmpApi.fetchAndPostForm(url, {}).then(() => {
return tmpApi.fetchAndPostForm(url, {}).then(() => {
location.reload()
})
},

View File

@ -115,6 +115,24 @@
'actionButton' => $this->element('genericElements/Form/submitButton', $submitButtonData),
'class' => 'modal-lg'
]);
} else if (!empty($raw)) {
echo sprintf(
'%s%s%s%s%s%s',
empty($data['description']) ? '' : sprintf(
'<div class="pb-2">%s</div>',
$data['description']
),
$ajaxFlashMessage,
$formCreate,
$fieldsString,
empty($metaTemplateString) ? '' : $this->element(
'genericElements/accordion_scaffold', [
'body' => $metaTemplateString,
'title' => 'Meta fields'
]
),
$formEnd
);
} else {
echo sprintf(
'%s<h2>%s</h2>%s%s%s%s%s%s%s%s%s',

View File

@ -3,7 +3,7 @@
echo sprintf(
'%s',
sprintf(
'<button id="submitButton" class="btn btn-primary" data-form-id="%s" autofocus>%s</button>',
'<button id="submitButton" class="btn btn-primary" data-form-id="%s" type="button" autofocus>%s</button>',
'#form-' . h($formRandomValue),
__('Submit')
)

View File

@ -97,7 +97,7 @@
);
}
$reload_url = !empty($action['reload_url']) ? $action['reload_url'] : $this->Url->build(['action' => 'index']);
$action['onclick'] = sprintf('UI.openModalFromURL(\'%s\', \'%s\', \'%s\')', $modal_url, $reload_url, $tableRandomValue);
$action['onclick'] = sprintf('UI.submissionModalForIndex(\'%s\', \'%s\', \'%s\')', $modal_url, $reload_url, $tableRandomValue);
}
echo sprintf(
'<a href="%s" title="%s" aria-label="%s" %s %s class="link-unstyled"><i class="%s"></i></a> ',

View File

@ -14,7 +14,7 @@ if ($field['scope'] === 'individuals') {
h($alignment['organisation']['name'])
),
!$canRemove ? '' : sprintf(
"UI.openModalFromURL(%s);",
"UI.submissionModalForIndex(%s);",
sprintf(
"'/alignments/delete/%s'",
h($alignment['id'])
@ -34,7 +34,7 @@ if ($field['scope'] === 'individuals') {
h($alignment['individual']['email'])
),
!$canRemove ? '' : sprintf(
"UI.openModalFromURL(%s);",
"UI.submissionModalForIndex(%s);",
sprintf(
"'/alignments/delete/%s'",
h($alignment['id'])

View File

@ -1,5 +1,5 @@
<?php
$type = $this->Hash->extract($row, $field['owner_type_path'])[0];
$type = $this->Hash->extract($row, $field['owner_model_path'])[0];
$owner = $row[$type];
$types = [
'individual' => [

View File

@ -0,0 +1,11 @@
<?php
if (!empty($row['user'])) {
$userId = $this->Hash->extract($row, 'user.id')[0];
$userName = $this->Hash->extract($row, 'user.username')[0];
echo $this->Html->link(
h($userName),
['controller' => 'users', 'action' => 'view', $userId]
);
}
?>

View File

@ -71,12 +71,8 @@
<script>
function openModalForButton(clicked, url, reloadUrl='') {
const loadingOverlay = new OverlayFactory(clicked);
const fallbackReloadUrl = '<?= $this->Url->build(['action' => 'index']); ?>'
reloadUrl = reloadUrl != '' ? reloadUrl : fallbackReloadUrl
loadingOverlay.show()
UI.openModalFromURL(url, reloadUrl, '<?= $tableRandomValue ?>').finally(() => {
loadingOverlay.hide()
})
UI.overlayUntilResolve(clicked, UI.submissionModalForIndex(url, reloadUrl, '<?= $tableRandomValue ?>'))
}
</script>

View File

@ -12,6 +12,7 @@
* - id: element ID for the input field - defaults to quickFilterField
*/
if (!isset($data['requirement']) || $data['requirement']) {
$filterEffective = !empty($quickFilter); // No filters will be picked up, thus rendering the filtering useless
$filteringButton = '';
if (!empty($data['allowFilering'])) {
$activeFilters = !empty($activeFilters) ? $activeFilters : [];
@ -32,9 +33,10 @@
$filteringButton = $this->Bootstrap->button($buttonConfig);
}
$button = empty($data['button']) && empty($data['fa-icon']) ? '' : sprintf(
'<div class="input-group-append"><button class="btn btn-primary" %s id="quickFilterButton-%s">%s%s</button>%s</div>',
'<div class="input-group-append"><button class="btn btn-primary" %s id="quickFilterButton-%s" %s>%s%s</button>%s</div>',
empty($data['data']) ? '' : h($data['data']),
h($tableRandomValue),
$filterEffective ? '' : 'disabled="disabled"',
empty($data['fa-icon']) ? '' : sprintf('<i class="fa fa-%s"></i>', h($data['fa-icon'])),
empty($data['button']) ? '' : h($data['button']),
$filteringButton
@ -43,13 +45,14 @@
$button .= $this->element('/genericElements/ListTopBar/element_simple', array('data' => $data['cancel']));
}
$input = sprintf(
'<input id="quickFilterField-%s" type="text" class="form-control" placeholder="%s" aria-label="%s" style="padding: 2px 6px;" id="%s" data-searchkey="%s" value="%s">',
'<input id="quickFilterField-%s" type="text" class="form-control" placeholder="%s" aria-label="%s" style="padding: 2px 6px;" id="%s" data-searchkey="%s" value="%s" %s>',
h($tableRandomValue),
empty($data['placeholder']) ? '' : h($data['placeholder']),
empty($data['placeholder']) ? '' : h($data['placeholder']),
empty($data['id']) ? 'quickFilterField' : h($data['id']),
empty($data['searchKey']) ? 'searchall' : h($data['searchKey']),
empty($data['value']) ? (!empty($quickFilterValue) ? h($quickFilterValue) : '') : h($data['value'])
empty($data['value']) ? (!empty($quickFilterValue) ? h($quickFilterValue) : '') : h($data['value']),
$filterEffective ? '' : 'disabled="disabled"'
);
echo sprintf(
'<div class="input-group" data-table-random-value="%s" style="margin-left: auto;">%s%s</div>',
@ -127,7 +130,8 @@
}
$searchType = $('<span/>')
.text(searchContain ? '<?= __('Contain') ?>' : '<?= __('Exact match') ?>')
.attr('title', searchContain ? '<?= __('The search value will be used as a substring') ?>' : '<?= __('The search value must strictly match') ?>')
.attr('title', searchContain ? '<?= __('The search value can be used as a substring with the wildcard operator `%`') ?>' : '<?= __('The search value must strictly match') ?>')
.attr('style', 'cursor: help;')
tableData.push([fieldName, $searchType])
});
tableData.sort((a, b) => a[0] < b[0] ? -1 : 1)
@ -144,11 +148,7 @@
}
function openFilteringModal(clicked, url, reloadUrl, tableId) {
const loadingOverlay = new OverlayFactory(clicked);
loadingOverlay.show()
UI.openModalFromURL(url, reloadUrl, tableId).finally(() => {
loadingOverlay.hide()
})
UI.overlayUntilResolve(clicked, UI.submissionModalForIndex(url, reloadUrl, tableId))
}
});
</script>

View File

@ -5,7 +5,7 @@
$elements .= $this->element('/genericElements/ListTopBar/element_' . (empty($element['type']) ? 'simple' : h($element['type'])), array('data' => $element, 'tableRandomValue' => $tableRandomValue));
}
echo sprintf(
'<div %s class="btn-group btn-group-sm mr-2" role="group" aria-label="button-group">%s</div>',
'<div %s class="btn-group btn-group-sm mr-2 flex-wrap" role="group" aria-label="button-group">%s</div>',
(!empty($data['id'])) ? 'id="' . h($data['id']) . '"' : '',
$elements
);

View File

@ -20,7 +20,7 @@ if ($field['scope'] === 'individuals') {
h($alignment['organisation']['name'])
),
sprintf(
"UI.openModalFromURL(%s);",
"UI.submissionModalForSinglePage(%s);",
sprintf(
"'/alignments/delete/%s'",
$alignment['id']
@ -40,7 +40,7 @@ if ($field['scope'] === 'individuals') {
h($alignment['individual']['email'])
),
sprintf(
"UI.openModalFromURL(%s);",
"UI.submissionModalForSinglePage(%s);",
sprintf(
"'/alignments/delete/%s'",
$alignment['id']
@ -53,7 +53,7 @@ echo sprintf(
'<div class="alignments-list">%s</div><div class="alignments-add-container"><button class="alignments-add-button btn btn-primary btn-sm" onclick="%s">%s</button></div>',
$alignments,
sprintf(
"UI.openModalFromURL('/alignments/add/%s/%s');",
"UI.submissionModalForSinglePage('/alignments/add/%s/%s');",
h($field['scope']),
h($extracted['id'])
),

View File

@ -35,7 +35,7 @@ if (isset($menu[$metaGroup])) {
}
$active = ($scope === $this->request->getParam('controller') && $action === $this->request->getParam('action'));
if (!empty($data['popup'])) {
$link_template = '<a href="#" onClick="UI.openModalFromURL(\'%s\')" class="list-group-item list-group-item-action %s %s pl-3 border-0 %s">%s</a>';
$link_template = '<a href="#" onClick="UI.submissionModalAutoGuess(\'%s\')" class="list-group-item list-group-item-action %s %s pl-3 border-0 %s">%s</a>';
} else {
$link_template = '<a href="%s" class="list-group-item list-group-item-action %s %s pl-3 border-0 %s">%s</a>';
}

View File

@ -13,7 +13,7 @@
<?= $this->Form->postLink(
h($actionName),
$path,
['class' => 'btn btn-primary button-execute']
['class' => 'btn btn-primary button-execute', 'id' => 'submitButton']
)
?>
<button type="button" class="btn btn-secondary cancel-button" data-dismiss="modal"><?= __('Cancel') ?></button>

View File

@ -1,7 +1,13 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><?= __('Delete {0}', h(Cake\Utility\Inflector::singularize(Cake\Utility\Inflector::humanize($this->request->getParam('controller'))))) ?></h5>
<h5 class="modal-title">
<?php if (empty($deletionTitle)): ?>
<p><?= __('Delete {0}', h(Cake\Utility\Inflector::singularize(Cake\Utility\Inflector::humanize($this->request->getParam('controller'))))) ?></p>
<?php else: ?>
<p><?= h($deletionTitle) ?></p>
<?php endif; ?>
</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
@ -15,9 +21,9 @@
</div>
<div class="modal-footer">
<?= $this->Form->postLink(
'Delete',
!empty($deletionConfirm) ? h($deletionConfirm) : __('Delete'),
(empty($postLinkParameters) ? ['action' => 'delete', $id] : $postLinkParameters),
['class' => 'btn btn-primary button-execute', 'id' => 'submitButton']
['class' => 'btn btn-danger button-execute', 'id' => 'submitButton']
)
?>
<button type="button" class="btn btn-secondary cancel-button" data-dismiss="modal"><?= __('Cancel') ?></button>

View File

@ -15,7 +15,7 @@ $filteringForm = $this->Bootstrap->table(
[
'labelHtml' => sprintf('%s %s',
__('Value'),
sprintf('<span class="fa fa-info ml-1" title="%s"><span>', __('Supports strict match and LIKE match with the `%` character.&#10;Example: `%.com`'))
sprintf('<sup class="fa fa-info" title="%s"><sup>', __('Supports strict match and LIKE match with the `%` character.&#10;Example: `%.com`'))
)
],
__('Action')
@ -30,7 +30,7 @@ echo $this->Bootstrap->modal([
'type' => 'confirm',
'bodyHtml' => $filteringForm,
'confirmText' => __('Filter'),
'confirmFunction' => 'filterIndex(this)'
'confirmFunction' => 'filterIndex'
]);
?>
@ -40,11 +40,10 @@ echo $this->Bootstrap->modal([
initFilteringTable($filteringTable)
})
function filterIndex(clicked) {
function filterIndex(modalObject, tmpApi) {
const controller = '<?= $this->request->getParam('controller') ?>';
const action = 'index';
const $clicked = $(clicked)
const $tbody = $clicked.closest('div.modal-content').find('table.indexFilteringTable tbody')
const $tbody = modalObject.$modal.find('table.indexFilteringTable tbody')
const $rows = $tbody.find('tr:not(#controlRow)')
const activeFilters = {}
$rows.each(function() {

View File

@ -0,0 +1,12 @@
<?php
echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'skip_pagination' => true,
'data' => !empty($data) ? $data : [],
'top_bar' => [],
'fields' => !empty($fields) ? $fields : [],
'title' => !empty($title) ? h($title) : __('Index'),
'description' => !empty($description) ? h($description) : '',
'actions' => !empty($actions) ? $actions : []
],
]);

View File

@ -134,6 +134,18 @@ class AJAXApi {
return tmpApi.postForm(form, dataToMerge, constAlteredOptions.skipRequestHooks)
}
/**
* @param {string} url - The URL to on which to execute the POST
* @param {Object} [data={}] - The data to be posted
* @param {Object} [options={}] - The options supported by AJAXApi#defaultOptions
* @return {Promise<Object>} Promise object resolving to the result of the POST operation
*/
static async quickPostData(url, data={}, options={}) {
const constAlteredOptions = Object.assign({}, {}, options)
const tmpApi = new AJAXApi(constAlteredOptions)
return tmpApi.postData(url, data, constAlteredOptions.skipRequestHooks)
}
/**
* @param {string} url - The URL from which to fetch the form
* @param {Object} [dataToMerge={}] - Additional data to be integrated or modified in the form
@ -258,6 +270,61 @@ class AJAXApi {
return toReturn
}
/**
* @param {string} url - The URL to fetch
* @param {Object} dataToPost - data to be posted
* @param {boolean} [skipRequestHooks=false] - If true, default request hooks will be skipped
* @param {boolean} [skipFeedback=false] - Pass this value to the AJAXApi.provideFeedback function
* @return {Promise<string>} Promise object resolving to the result of the POST
*/
async postData(url, dataToPost, skipRequestHooks=false, skipFeedback=false) {
if (!skipRequestHooks) {
this.beforeRequest()
}
let toReturn
try {
let formData = new FormData()
formData = AJAXApi.mergeFormData(formData, dataToPost)
let requestConfig = AJAXApi.genericRequestConfigPOST
requestConfig.headers.append('AUTHORIZATION', '~HACKY-HACK~')
let options = {
...requestConfig,
body: formData,
};
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`Network response was not ok. \`${response.statusText}\``)
}
const data = await response.json()
if (data.success) {
this.provideFeedback({
variant: 'success',
body: data.message
}, false, skipFeedback);
toReturn = data;
} else {
this.provideFeedback({
variant: 'danger',
title: 'There has been a problem with the operation',
body: data.message
}, true, skipFeedback);
toReturn = Promise.reject(data.errors);
}
} catch (error) {
this.provideFeedback({
variant: 'danger',
title: 'There has been a problem with the operation',
body: error.message
}, true, skipFeedback);
toReturn = Promise.reject(error);
} finally {
if (!skipRequestHooks) {
this.afterRequest()
}
}
return toReturn
}
/**
* @param {HTMLFormElement} form - The form to be posted
* @param {Object} [dataToMerge={}] - Additional data to be integrated or modified in the form

View File

@ -25,20 +25,20 @@ class UIFactory {
}
/**
* Create and display a modal where the modal's content is fetched from the provided URL.
* Create and display a modal where the modal's content is fetched from the provided URL. Link an AJAXApi to the submission button
* @param {string} url - The URL from which the modal's content should be fetched
* @param {ModalFactory~POSTSuccessCallback} POSTSuccessCallback - The callback that handles successful form submission
* @param {ModalFactory~POSTFailCallback} POSTFailCallback - The callback that handles form submissions errors and validation errors.
* @return {Promise<Object>} Promise object resolving to the ModalFactory object
*/
modalFromURL(url, POSTSuccessCallback, POSTFailCallback) {
submissionModal(url, POSTSuccessCallback, POSTFailCallback) {
return AJAXApi.quickFetchURL(url).then((modalHTML) => {
const theModal = new ModalFactory({
rawHtml: modalHTML,
POSTSuccessCallback: POSTSuccessCallback !== undefined ? POSTSuccessCallback : () => {},
POSTFailCallback: POSTFailCallback !== undefined ? POSTFailCallback : (errorMessage) => {},
});
theModal.makeModal(modalHTML)
theModal.makeModal()
theModal.show()
theModal.$modal.data('modalObject', theModal)
return theModal
@ -46,45 +46,139 @@ class UIFactory {
}
/**
* Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the table after a successful operation and handles displayOnSuccess option
* Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the single page view after a successful operation.
* Supports `displayOnSuccess` option to show another modal after the submission
* @param {string} url - The URL from which the modal's content should be fetched
* @param {string} reloadUrl - The URL from which the data should be fetched after confirming
* @param {string} tableId - The table ID which should be reloaded on success
* @param {(boolean|string)} [reloadUrl=false] - The URL from which the data should be fetched after confirming
* @param {(jQuery|string)} [$table=false] - The table ID which should be reloaded on success
* @return {Promise<Object>} Promise object resolving to the ModalFactory object
*/
openModalFromURL(url, reloadUrl=false, tableId=false) {
return UI.modalFromURL(url, (data) => {
let reloaded = false
if (reloadUrl === false || tableId === false) { // Try to get information from the DOM
let $elligibleTable = $('table.table')
let currentModel = location.pathname.split('/')[1]
if ($elligibleTable.length == 1 && currentModel.length > 0) {
let $container = $elligibleTable.closest('div[id^="table-container-"]')
if ($container.length == 1) {
UI.reload(`/${currentModel}/index`, $container, $elligibleTable)
reloaded = true
} else {
$container = $elligibleTable.closest('div[id^="single-view-table-container-"]')
if ($container.length == 1) {
UI.reload(location.pathname, $container, $elligibleTable)
reloaded = true
}
}
}
submissionModalForSinglePage(url, reloadUrl=false, $table=false) {
let $statusNode, $reloadedElement
if (reloadUrl === false) {
reloadUrl = location.pathname
}
if ($table === false) { // Try to get information from the DOM
const $elligibleTable = $('table[id^="single-view-table-"]')
const $container = $elligibleTable.closest('div[id^="single-view-table-container-"]')
$reloadedElement = $container
$statusNode = $elligibleTable
} else {
if ($table instanceof jQuery) {
$reloadedElement = $table
$statusNode = $table.find('table[id^="single-view-table-"]')
} else {
UI.reload(reloadUrl, $(`#table-container-${tableId}`), $(`#table-container-${tableId} table.table`))
reloaded = true
$reloadedElement = $(`single-view-table-container-${$table}`)
$statusNode = $(`single-view-table-${$table}`)
}
}
if ($reloadedElement.length == 0) {
UI.toast({
variant: 'danger',
title: 'Could not find element to be reloaded',
body: 'The content of this page may have changed and has not been reflected. Reloading the page is advised.'
})
return
}
return UI.submissionReloaderModal(url, reloadUrl, $reloadedElement, $statusNode);
}
/**
* Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the index table after a successful operation.
* Supports `displayOnSuccess` option to show another modal after the submission
* @param {string} url - The URL from which the modal's content should be fetched
* @param {(boolean|string)} [reloadUrl=false] - The URL from which the data should be fetched after confirming
* @param {(jQuery|string)} [$table=false] - The table ID which should be reloaded on success
* @return {Promise<Object>} Promise object resolving to the ModalFactory object
*/
submissionModalForIndex(url, reloadUrl=false, $table=false) {
let $statusNode, $reloadedElement
if (reloadUrl === false) {
const currentModel = location.pathname.split('/')[1]
if (currentModel.length > 0) {
reloadUrl = `/${currentModel}/index`
} else {
UI.toast({
variant: 'danger',
title: 'Could not find URL for the reload',
body: 'The content of this page may have changed and has not been reflected. Reloading the page is advised.'
})
return
}
}
if ($table === false) { // Try to get information from the DOM
const $elligibleTable = $('table.table')
const $container = $elligibleTable.closest('div[id^="table-container-"]')
$reloadedElement = $container
$statusNode = $elligibleTable
} else {
if ($table instanceof jQuery) {
$reloadedElement = $table
$statusNode = $table.find('table.table')
} else {
$reloadedElement = $(`#table-container-${$table}`)
$statusNode = $(`#table-container-${$table} table.table`)
}
}
if ($reloadedElement.length == 0) {
UI.toast({
variant: 'danger',
title: 'Could not find element to be reloaded',
body: 'The content of this page may have changed and has not been reflected. Reloading the page is advised.'
})
return
}
return UI.submissionReloaderModal(url, reloadUrl, $reloadedElement, $statusNode);
}
/**
* Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the index table after a successful operation.
* Supports `displayOnSuccess` option to show another modal after the submission
* @param {string} url - The URL from which the modal's content should be fetched
* @param {(boolean|string)} [reloadUrl=false] - The URL from which the data should be fetched after confirming
* @param {(jQuery|string)} [$table=false] - The table ID which should be reloaded on success
* @return {Promise<Object>} Promise object resolving to the ModalFactory object
*/
submissionModalAutoGuess(url, reloadUrl=false, $table=false) {
let currentAction = location.pathname.split('/')[2]
currentAction += 'cdsc'
if (currentAction !== undefined) {
if (currentAction === 'index') {
return UI.submissionModalForIndex(url, reloadUrl, $table)
} else if (currentAction === 'view') {
return UI.submissionModalForSinglePage(url, reloadUrl, $table)
}
}
const successCallback = () => {
UI.toast({
variant: 'danger',
title: 'Could not reload the page',
body: 'Reloading the page manually is advised.'
})
}
return UI.submissionModal(url, successCallback)
}
/**
* Creates and displays a modal where the modal's content is fetched from the provided URL. Reloads the provided element after a successful operation.
* Supports `displayOnSuccess` option to show another modal after the submission
* @param {string} url - The URL from which the modal's content should be fetched
* @param {string} reloadUrl - The URL from which the data should be fetched after confirming
* @param {(jQuery|string)} $reloadedElement - The element which should be reloaded on success
* @param {(jQuery|string)} [$statusNode=null] - A reference to a HTML node on which the loading animation should be displayed. If not provided, $container will be used
* @return {Promise<Object>} Promise object resolving to the ModalFactory object
*/
submissionReloaderModal(url, reloadUrl, $reloadedElement, $statusNode=null) {
const successCallback = function (data) {
UI.reload(reloadUrl, $reloadedElement, $statusNode)
if (data.additionalData !== undefined && data.additionalData.displayOnSuccess !== undefined) {
UI.modal({
rawHtml: data.additionalData.displayOnSuccess
})
} else {
if (!reloaded) {
location.reload()
}
}
})
}
return UI.submissionModal(url, successCallback)
}
/**
@ -255,9 +349,9 @@ class Toaster {
if (options.body !== false || options.bodyHtml !== false) {
var $toastBody
if (options.bodyHtml !== false) {
$toastBody = $('<div class="toast-body"/>').html(options.mutedHtml)
$toastBody = $('<div class="toast-body"/>').html(options.bodyHtml)
} else {
$toastBody = $('<div class="toast-body"/>').text(options.body)
$toastBody = $('<div class="toast-body"/>').append($('<div style="white-space: break-spaces;"/>').text(options.body))
}
$toast.append($toastBody)
}
@ -273,8 +367,15 @@ class ModalFactory {
*/
constructor(options) {
this.options = Object.assign({}, ModalFactory.defaultOptions, options)
if (this.options.rawHtml && options.POSTSuccessCallback !== undefined) {
this.attachSubmitButtonListener = true
if (options.POSTSuccessCallback !== undefined) {
if (this.options.rawHtml) {
this.attachSubmitButtonListener = true
} else {
UI.toast({
variant: 'danger',
bodyHtml: '<b>POSTSuccessCallback</b> can only be used in conjuction with the <i>rawHtml</i> option. Instead, use the promise instead returned by the API call in <b>APIConfirm</b>.'
})
}
}
if (options.type === undefined && options.cancel !== undefined) {
this.options.type = 'confirm'
@ -349,14 +450,14 @@ class ModalFactory {
* @property {string} confirmText - The text to be placed in the confirm button
* @property {string} cancelText - The text to be placed in the cancel button
* @property {boolean} closeManually - If true, the modal will be closed automatically whenever a footer's button is pressed
* @property {boolean} closeOnSuccess - If true, the modal will be closed if the $FILL_ME operation is successful
* @property {boolean} closeOnSuccess - If true, the modal will be closed if the operation is successful
* @property {ModalFactory~confirm} confirm - The callback that should be called if the user confirm the modal
* @property {ModalFactory~cancel} cancel - The callback that should be called if the user cancel the modal
* @property {ModalFactory~APIConfirm} APIConfirm - The callback that should be called if the user confirm the modal. Behaves like the confirm option but provides an AJAXApi object that can be used to issue requests
* @property {ModalFactory~APIError} APIError - The callback called if the APIConfirm callback fails.
* @property {ModalFactory~shownCallback} shownCallback - The callback that should be called whenever the modal is shown
* @property {ModalFactory~hiddenCallback} hiddenCallback - The callback that should be called whenever the modal is hiddenAPIConfirm
* @property {ModalFactory~POSTSuccessCallback} POSTSuccessCallback - The callback that should be called if the POST operation has been a success
* @property {ModalFactory~POSTSuccessCallback} POSTSuccessCallback - The callback that should be called if the POST operation has been a success. Works in confunction with the `rawHtml`
* @property {ModalFactory~POSTFailCallback} POSTFailCallback - The callback that should be called if the POST operation has been a failure (Either the request failed or the form validation did not pass)
*/
static defaultOptions = {
@ -587,8 +688,11 @@ class ModalFactory {
}
/** Attach the submission click listener for modals that have been generated by raw HTML */
findSubmitButtonAndAddListener(clearOnclick=true) {
const $submitButton = this.$modal.find('.modal-footer #submitButton')
findSubmitButtonAndAddListener() {
let $submitButton = this.$modal.find('.modal-footer #submitButton')
if (!$submitButton[0]) {
$submitButton = this.$modal.find('.modal-footer .modal-confirm-button')
}
if ($submitButton[0]) {
const formID = $submitButton.data('form-id')
let $form
@ -597,26 +701,46 @@ class ModalFactory {
} else {
$form = this.$modal.find('form')
}
if (clearOnclick) {
if ($submitButton.data('confirmfunction') !== undefined && $submitButton.data('confirmfunction') !== '') {
const clickHandler = window[$submitButton.data('confirmfunction')]
this.options.APIConfirm = (tmpApi) => {
let clickResult = clickHandler(this, tmpApi)
if (clickResult !== undefined) {
return clickResult
.then((data) => {
if (data.success) {
this.options.POSTSuccessCallback(data)
} else { // Validation error
this.injectFormValidationFeedback(form, data.errors)
return Promise.reject('Validation error');
}
})
.catch((errorMessage) => {
this.options.POSTFailCallback(errorMessage)
return Promise.reject(errorMessage);
})
}
}
} else {
$submitButton[0].removeAttribute('onclick')
}
this.options.APIConfirm = (tmpApi) => {
return tmpApi.postForm($form[0])
.then((data) => {
if (data.success) {
this.options.POSTSuccessCallback(data)
} else { // Validation error
this.injectFormValidationFeedback(form, data.errors)
return Promise.reject('Validation error');
}
})
.catch((errorMessage) => {
this.options.POSTFailCallback(errorMessage)
return Promise.reject(errorMessage);
})
this.options.APIConfirm = (tmpApi) => {
return tmpApi.postForm($form[0])
.then((data) => {
if (data.success) {
this.options.POSTSuccessCallback(data)
} else { // Validation error
this.injectFormValidationFeedback(form, data.errors)
return Promise.reject('Validation error');
}
})
.catch((errorMessage) => {
this.options.POSTFailCallback(errorMessage)
return Promise.reject(errorMessage);
})
}
}
$submitButton.click(this.getConfirmationHandlerFunction())
}
}
}