Merge pull request #89 from cerebrate-project/develop-unstable
Features extension packcli-modification-summary
commit
4f68585ac8
|
@ -5,6 +5,7 @@ logs
|
|||
tmp
|
||||
vendor
|
||||
webroot/theme/node_modules
|
||||
webroot/scss/*.css
|
||||
.vscode
|
||||
docker/run/
|
||||
.phpunit.result.cache
|
||||
|
|
|
@ -19,9 +19,10 @@
|
|||
"cakephp/bake": "^2.0.3",
|
||||
"cakephp/cakephp-codesniffer": "~4.0.0",
|
||||
"cakephp/debug_kit": "^4.0",
|
||||
"cebe/php-openapi": "^1.6",
|
||||
"fzaninotto/faker": "^1.9",
|
||||
"josegonzalez/dotenv": "^3.2",
|
||||
"league/openapi-psr7-validator": "^0.16.4",
|
||||
"league/openapi-psr7-validator": "^0.17",
|
||||
"phpunit/phpunit": "^8.5",
|
||||
"psy/psysh": "@stable",
|
||||
"wiremock-php/wiremock-php": "^2.32"
|
||||
|
@ -58,6 +59,11 @@
|
|||
"nohup sh ./tests/Helper/wiremock/start.sh >/dev/null 2>&1 &",
|
||||
"phpunit",
|
||||
"sh ./tests/Helper/wiremock/stop.sh"
|
||||
],
|
||||
"migrate": [
|
||||
"./bin/cake migrations migrate",
|
||||
"./bin/cake migrations migrate -p tags",
|
||||
"./bin/cake migrations migrate -p ADmad/SocialAuth"
|
||||
]
|
||||
},
|
||||
"prefer-stable": true,
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Migrations\AbstractMigration;
|
||||
use Phinx\Db\Adapter\MysqlAdapter;
|
||||
|
||||
class MailingLists extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* More information on this method is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
|
||||
* @return void
|
||||
*/
|
||||
|
||||
public $autoId = false; // turn off automatic `id` column create. We want it to be `int(10) unsigned`
|
||||
|
||||
|
||||
public function change()
|
||||
{
|
||||
$mailinglists = $this->table('mailing_lists', [
|
||||
'signed' => false,
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
]);
|
||||
$mailinglists
|
||||
->addColumn('id', 'integer', [
|
||||
'autoIncrement' => true,
|
||||
'limit' => 10,
|
||||
'signed' => false,
|
||||
])
|
||||
->addPrimaryKey('id')
|
||||
->addColumn('uuid', 'uuid', [
|
||||
'default' => null,
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('name', 'string', [
|
||||
'default' => null,
|
||||
'null' => false,
|
||||
'limit' => 191,
|
||||
'comment' => 'The name of the mailing list',
|
||||
])
|
||||
->addColumn('recipients', 'string', [
|
||||
'default' => null,
|
||||
'null' => true,
|
||||
'limit' => 191,
|
||||
'comment' => 'Human-readable description of who the intended recipients.',
|
||||
])
|
||||
->addColumn('description', 'text', [
|
||||
'default' => null,
|
||||
'null' => true,
|
||||
'comment' => 'Additional description of the mailing list'
|
||||
])
|
||||
->addColumn('user_id', 'integer', [
|
||||
'default' => null,
|
||||
'null' => true,
|
||||
'signed' => false,
|
||||
'length' => 10,
|
||||
])
|
||||
->addColumn('active', 'boolean', [
|
||||
'default' => 0,
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('deleted', 'boolean', [
|
||||
'default' => 0,
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('created', 'datetime', [
|
||||
'default' => null,
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('modified', 'datetime', [
|
||||
'default' => null,
|
||||
'null' => false,
|
||||
]);
|
||||
|
||||
|
||||
$mailinglists->addForeignKey('user_id', 'users', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']);
|
||||
|
||||
$mailinglists->addIndex(['uuid'], ['unique' => true])
|
||||
->addIndex('name')
|
||||
->addIndex('recipients')
|
||||
->addIndex('user_id')
|
||||
->addIndex('active')
|
||||
->addIndex('deleted')
|
||||
->addIndex('created')
|
||||
->addIndex('modified');
|
||||
|
||||
$mailinglists->create();
|
||||
|
||||
|
||||
$mailinglists_individuals = $this->table('mailing_lists_individuals', [
|
||||
'signed' => false,
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
]);
|
||||
|
||||
$mailinglists_individuals
|
||||
->addColumn('id', 'integer', [
|
||||
'autoIncrement' => true,
|
||||
'limit' => 10,
|
||||
'signed' => false,
|
||||
])
|
||||
->addPrimaryKey('id')
|
||||
->addColumn('mailing_list_id', 'integer', [
|
||||
'default' => null,
|
||||
'null' => true,
|
||||
'signed' => false,
|
||||
'length' => 10,
|
||||
])
|
||||
->addColumn('individual_id', 'integer', [
|
||||
'default' => null,
|
||||
'null' => true,
|
||||
'signed' => false,
|
||||
'length' => 10,
|
||||
])
|
||||
->addColumn('include_primary_email', 'boolean', [
|
||||
'default' => 1,
|
||||
'null' => false,
|
||||
'comment' => 'Should the primary email address by included in the mailing list'
|
||||
])
|
||||
->addForeignKey('mailing_list_id', 'mailing_lists', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
|
||||
->addForeignKey('individual_id', 'individuals', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']);
|
||||
|
||||
$mailinglists_individuals->addIndex(['mailing_list_id', 'individual_id'], ['unique' => true]);
|
||||
|
||||
$mailinglists_individuals->create();
|
||||
|
||||
|
||||
$mailinglists_metafields = $this->table('mailing_lists_meta_fields', [
|
||||
'signed' => false,
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
]);
|
||||
|
||||
$mailinglists_metafields
|
||||
->addColumn('mailing_list_id', 'integer', [
|
||||
'default' => null,
|
||||
'null' => true,
|
||||
'signed' => false,
|
||||
'length' => 10,
|
||||
])
|
||||
->addColumn('meta_field_id', 'integer', [
|
||||
'default' => null,
|
||||
'null' => true,
|
||||
'signed' => false,
|
||||
'length' => 10,
|
||||
])
|
||||
->addPrimaryKey(['mailing_list_id', 'meta_field_id'])
|
||||
->addForeignKey('mailing_list_id', 'mailing_lists', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE'])
|
||||
->addForeignKey('meta_field_id', 'meta_fields', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE']);
|
||||
|
||||
$mailinglists_metafields->create();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Migrations\AbstractMigration;
|
||||
use Phinx\Db\Adapter\MysqlAdapter;
|
||||
|
||||
class MoreMetaFieldColumns extends AbstractMigration
|
||||
{
|
||||
public function change()
|
||||
{
|
||||
$metaFieldsTable = $this->table('meta_fields');
|
||||
|
||||
$metaFieldsTable
|
||||
->addColumn('created', 'datetime', [
|
||||
'default' => null,
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('modified', 'datetime', [
|
||||
'default' => null,
|
||||
'null' => false,
|
||||
])
|
||||
->update();
|
||||
|
||||
$metaFieldsTable
|
||||
->addIndex('created')
|
||||
->addIndex('modified');
|
||||
|
||||
$metaTemplateFieldsTable = $this->table('meta_template_fields')
|
||||
->addColumn('counter', 'integer', [
|
||||
'default' => 0,
|
||||
'length' => 11,
|
||||
'null' => false,
|
||||
'signed' => false,
|
||||
'comment' => 'Field used by the CounterCache behaviour to count the occurence of meta_template_fields'
|
||||
])
|
||||
->addForeignKey('meta_template_id', 'meta_templates', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
|
||||
->update();
|
||||
|
||||
$metaTemplate = $this->table('meta_templates')
|
||||
->removeIndex(['uuid'])
|
||||
->addIndex(['uuid', 'version'])
|
||||
->update();
|
||||
}
|
||||
}
|
|
@ -159,9 +159,9 @@ class LocalToolInboxProcessor extends GenericInboxProcessor
|
|||
{
|
||||
return $validator
|
||||
->requirePresence('connectorName')
|
||||
->notEmpty('connectorName', 'The connector name must be provided')
|
||||
->notEmptyString('connectorName', 'The connector name must be provided')
|
||||
->requirePresence('cerebrateURL')
|
||||
->notEmpty('cerebrateURL', 'A url must be provided')
|
||||
->notEmptyString('cerebrateURL', 'A url must be provided')
|
||||
->requirePresence('local_tool_id')
|
||||
->numeric('local_tool_id', 'A local_tool_id must be provided')
|
||||
->requirePresence('remote_tool_id')
|
||||
|
|
|
@ -37,13 +37,13 @@ class RegistrationProcessor extends UserInboxProcessor implements GenericInboxPr
|
|||
protected function addValidatorRules($validator)
|
||||
{
|
||||
return $validator
|
||||
->notEmpty('username', 'A username must be provided.')
|
||||
->notEmptyString('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')
|
||||
->notEmptyString('first_name', 'A first name must be provided')
|
||||
->notEmptyString('last_name', 'A last name must be provided')
|
||||
->add('password', 'password_complexity', [
|
||||
'rule' => function($value, $context) {
|
||||
if (!preg_match('/^((?=.*\d)|(?=.*\W+))(?![\n])(?=.*[A-Z])(?=.*[a-z]).*$|.{16,}/s', $value) || strlen($value) < 12) {
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "CSIRT Constituency",
|
||||
"namespace": "cerebrate",
|
||||
"description": "Template meant to collect data about the constituency of a CSIRT",
|
||||
"version": 1,
|
||||
"scope": "organisation",
|
||||
"uuid": "faca6acc-23e0-4585-8fd8-4379e3a6250d",
|
||||
"source": "Cerebrate",
|
||||
"metaFields": [
|
||||
{
|
||||
"field": "IPv4 address",
|
||||
"type": "ipv4",
|
||||
"multiple": true
|
||||
},
|
||||
{
|
||||
"field": "IPv6 address",
|
||||
"type": "ipv6",
|
||||
"multiple": true
|
||||
},
|
||||
{
|
||||
"field": "AS Number",
|
||||
"type": "text",
|
||||
"multiple": true
|
||||
},
|
||||
{
|
||||
"field": "Domain",
|
||||
"type": "text",
|
||||
"multiple": true
|
||||
},
|
||||
{
|
||||
"field": "Country",
|
||||
"type": "text",
|
||||
"multiple": true
|
||||
},
|
||||
{
|
||||
"field": "Country Code",
|
||||
"type": "text",
|
||||
"multiple": true
|
||||
},
|
||||
{
|
||||
"field": "Sector",
|
||||
"type": "text",
|
||||
"multiple": true
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "Cerebrate Individuals extended",
|
||||
"namespace": "cerebrate",
|
||||
"description": "Template to extend fields of individuals",
|
||||
"version": 2,
|
||||
"scope": "individual",
|
||||
"uuid": "3bc374c8-3cdd-4900-823e-cc9100ad5179",
|
||||
"source": "Cerebrate",
|
||||
"metaFields": [
|
||||
{
|
||||
"field": "alternate_email",
|
||||
"type": "text",
|
||||
"multiple": true
|
||||
},
|
||||
{
|
||||
"field": "mobile_phone",
|
||||
"type": "text",
|
||||
"multiple": true
|
||||
}
|
||||
]
|
||||
}
|
|
@ -9,7 +9,7 @@
|
|||
{
|
||||
"field": "website",
|
||||
"type": "text",
|
||||
"regex": "(http(s)?:\\\/\\\/.)?(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&\/\/=]*)"
|
||||
"regex": "https?:\\\/\\\/.+"
|
||||
},
|
||||
{
|
||||
"field": "enisa-geo-group",
|
||||
|
@ -50,7 +50,7 @@
|
|||
{
|
||||
"field": "email",
|
||||
"type": "text",
|
||||
"regex": "(?:[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])"
|
||||
"regex": "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+"
|
||||
},
|
||||
{
|
||||
"field": "country-name",
|
||||
|
@ -70,5 +70,5 @@
|
|||
"scope": "organisation",
|
||||
"source": "enisa.europa.eu/topics/csirts-in-europe/csirt-inventory/certs-by-country-interactive-map",
|
||||
"uuid": "089c68c7-d97e-4f21-a798-159cd10f7864",
|
||||
"version": 1
|
||||
}
|
||||
"version": 2
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "IT infrastructure and services",
|
||||
"namespace": "cerebrate",
|
||||
"description": "Offers the possiblity to register the IP of part of the infrastructure or services.",
|
||||
"version": 2,
|
||||
"scope": "organisation",
|
||||
"uuid": "a7674718-57c8-40e7-969e-d26ca911cb4a",
|
||||
"source": "Cerebrate",
|
||||
"metaFields": [
|
||||
{
|
||||
"field": "Microsoft Exchange Server IP",
|
||||
"type": "text",
|
||||
"multiple": true
|
||||
},
|
||||
{
|
||||
"field": "Microsfot Office 365 IP",
|
||||
"type": "text",
|
||||
"multiple": true
|
||||
},
|
||||
{
|
||||
"field": "Microsoft SharePoint IP",
|
||||
"type": "text",
|
||||
"multiple": true
|
||||
},
|
||||
{
|
||||
"field": "Microsoft Active Directory IP",
|
||||
"type": "text",
|
||||
"multiple": true
|
||||
},
|
||||
{
|
||||
"field": "Proxy IP",
|
||||
"type": "text",
|
||||
"multiple": true
|
||||
}
|
||||
]
|
||||
}
|
|
@ -33,7 +33,7 @@ class TagHelper extends Helper
|
|||
'data-text-colour' => h($tag['text_colour']),
|
||||
];
|
||||
}, $options['allTags']) : [];
|
||||
$classes = ['tag-input', 'flex-grow-1'];
|
||||
$classes = ['select2-input', 'flex-grow-1'];
|
||||
$url = '';
|
||||
if (!empty($this->getConfig('editable'))) {
|
||||
$url = $this->Url->build([
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
'type' => 'color',
|
||||
),
|
||||
),
|
||||
'metaTemplates' => empty($metaTemplates) ? [] : $metaTemplates,
|
||||
'submit' => array(
|
||||
'action' => $this->request->getParam('action')
|
||||
)
|
||||
|
|
|
@ -20,7 +20,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
|
|
|
@ -23,7 +23,7 @@ function createTagPicker(clicked) {
|
|||
|
||||
const $clicked = $(clicked)
|
||||
const $container = $clicked.closest('.tag-container')
|
||||
const $select = $container.parent().find('select.tag-input').removeClass('d-none')
|
||||
const $select = $container.parent().find('select.select2-input').removeClass('d-none')
|
||||
closePicker($select, $container)
|
||||
const $pickerContainer = $('<div></div>').addClass(['picker-container', 'd-flex'])
|
||||
|
||||
|
@ -90,7 +90,7 @@ function refreshTagList(apiResult, $container) {
|
|||
}
|
||||
|
||||
function initSelect2Pickers() {
|
||||
$('select.tag-input').each(function() {
|
||||
$('select.select2-input').each(function() {
|
||||
if (!$(this).hasClass("select2-hidden-accessible")) {
|
||||
initSelect2Picker($(this))
|
||||
}
|
||||
|
|
|
@ -79,6 +79,9 @@ class AppController extends Controller
|
|||
$this->loadComponent('Navigation', [
|
||||
'request' => $this->request,
|
||||
]);
|
||||
$this->loadComponent('Notification', [
|
||||
'request' => $this->request,
|
||||
]);
|
||||
if (Configure::read('debug')) {
|
||||
Configure::write('DebugKit.panels', ['DebugKit.Packages' => true]);
|
||||
Configure::write('DebugKit.forceEnable', true);
|
||||
|
@ -112,7 +115,6 @@ class AppController extends Controller
|
|||
}
|
||||
unset($user['password']);
|
||||
$this->ACL->setUser($user);
|
||||
$this->Navigation->genBreadcrumbs($user);
|
||||
$this->request->getSession()->write('authUser', $user);
|
||||
$this->isAdmin = $user['role']['perm_admin'];
|
||||
if (!$this->ParamHandler->isRest()) {
|
||||
|
@ -135,7 +137,6 @@ class AppController extends Controller
|
|||
|
||||
$this->ACL->checkAccess();
|
||||
if (!$this->ParamHandler->isRest()) {
|
||||
$this->set('breadcrumb', $this->Navigation->getBreadcrumb());
|
||||
$this->set('ajax', $this->request->is('ajax'));
|
||||
$this->request->getParam('prefix');
|
||||
$this->set('baseurl', Configure::read('App.fullBaseUrl'));
|
||||
|
@ -154,6 +155,16 @@ class AppController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
public function beforeRender(EventInterface $event)
|
||||
{
|
||||
if (!empty($this->request->getAttribute('identity'))) {
|
||||
if (!$this->ParamHandler->isRest()) {
|
||||
$this->set('breadcrumb', $this->Navigation->getBreadcrumb());
|
||||
$this->set('notifications', $this->Notification->getNotifications());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function authApiUser(): void
|
||||
{
|
||||
if (!empty($_SERVER['HTTP_AUTHORIZATION']) && strlen($_SERVER['HTTP_AUTHORIZATION'])) {
|
||||
|
|
|
@ -11,8 +11,8 @@ use Cake\Core\Configure;
|
|||
|
||||
class AuditLogsController extends AppController
|
||||
{
|
||||
public $filterFields = ['model_id', 'model', 'request_action', 'user_id', 'title'];
|
||||
public $quickFilterFields = ['model', 'request_action', 'title'];
|
||||
public $filterFields = ['model_id', 'model', 'request_action', 'user_id', 'model_title'];
|
||||
public $quickFilterFields = ['model', 'request_action', 'model_title'];
|
||||
public $containFields = ['Users'];
|
||||
|
||||
public function index()
|
||||
|
@ -31,6 +31,11 @@ class AuditLogsController extends AppController
|
|||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'Administration');
|
||||
}
|
||||
|
||||
public function filtering()
|
||||
{
|
||||
$this->CRUD->filtering();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -47,7 +47,8 @@ class ACLComponent extends Component
|
|||
'view' => ['*']
|
||||
],
|
||||
'AuditLogs' => [
|
||||
'index' => ['perm_admin']
|
||||
'filtering' => ['perm_admin'],
|
||||
'index' => ['perm_admin'],
|
||||
],
|
||||
'AuthKeys' => [
|
||||
'add' => ['*'],
|
||||
|
@ -119,16 +120,31 @@ class ACLComponent extends Component
|
|||
'view' => ['perm_admin'],
|
||||
'viewConnector' => ['perm_admin']
|
||||
],
|
||||
'MailingLists' => [
|
||||
"add" => ['perm_org_admin'],
|
||||
"addIndividual" => ['perm_org_admin'],
|
||||
"delete" => ['perm_org_admin'],
|
||||
"edit" => ['perm_org_admin'],
|
||||
"index" => ['*'],
|
||||
"listIndividuals" => ['perm_org_admin'],
|
||||
"removeIndividual" => ['perm_org_admin'],
|
||||
"view" => ['*'],
|
||||
],
|
||||
'MetaTemplateFields' => [
|
||||
'index' => ['perm_admin']
|
||||
],
|
||||
'MetaTemplates' => [
|
||||
'createNewTemplate' => ['perm_admin'],
|
||||
'delete' => ['perm_admin'],
|
||||
'disable' => ['perm_admin'],
|
||||
'enable' => ['perm_admin'],
|
||||
'getMetaFieldsToUpdate' => ['perm_admin'],
|
||||
'index' => ['perm_admin'],
|
||||
'migrateOldMetaTemplateToNewestVersionForEntity' => ['perm_admin'],
|
||||
'update' => ['perm_admin'],
|
||||
'updateAllTemplates' => ['perm_admin'],
|
||||
'toggle' => ['perm_admin'],
|
||||
'view' => ['perm_admin']
|
||||
'view' => ['perm_admin'],
|
||||
],
|
||||
'Organisations' => [
|
||||
'add' => ['perm_admin'],
|
||||
|
@ -559,7 +575,6 @@ class ACLComponent extends Component
|
|||
continue;
|
||||
}
|
||||
}
|
||||
$menu[$group][$subMenuElementName]['children'] = array_values($menu[$group][$subMenuElementName]['children']);
|
||||
if (empty($menu[$group][$subMenuElementName]['children'])) {
|
||||
unset($subMenu[$subMenuElementName]);
|
||||
}
|
||||
|
|
|
@ -6,10 +6,14 @@ use Cake\Controller\Component;
|
|||
use Cake\Error\Debugger;
|
||||
use Cake\Utility\Hash;
|
||||
use Cake\Utility\Inflector;
|
||||
use Cake\Utility\Text;
|
||||
use Cake\View\ViewBuilder;
|
||||
use Cake\ORM\TableRegistry;
|
||||
use Cake\Routing\Router;
|
||||
use Cake\Http\Exception\MethodNotAllowedException;
|
||||
use Cake\Http\Exception\NotFoundException;
|
||||
use Cake\Collection\Collection;
|
||||
use App\Utility\UI\IndexSetting;
|
||||
|
||||
class CRUDComponent extends Component
|
||||
{
|
||||
|
@ -33,6 +37,8 @@ class CRUDComponent extends Component
|
|||
$options['filters'] = [];
|
||||
}
|
||||
$options['filters'][] = 'quickFilter';
|
||||
} else {
|
||||
$options['quickFilters'] = [];
|
||||
}
|
||||
$options['filters'][] = 'filteringLabel';
|
||||
if ($this->taggingSupported()) {
|
||||
|
@ -44,12 +50,13 @@ class CRUDComponent extends Component
|
|||
$optionFilters[] = "{$filter} !=";
|
||||
}
|
||||
$params = $this->Controller->ParamHandler->harvestParams($optionFilters);
|
||||
$params = $this->fakeContextFilter($options, $params);
|
||||
$query = $this->Table->find();
|
||||
if (!empty($options['filterFunction'])) {
|
||||
$query = $options['filterFunction']($query);
|
||||
}
|
||||
$query = $this->setFilters($params, $query, $options);
|
||||
$query = $this->setQuickFilters($params, $query, empty($options['quickFilters']) ? [] : $options['quickFilters']);
|
||||
$query = $this->setQuickFilters($params, $query, $options);
|
||||
if (!empty($options['conditions'])) {
|
||||
$query->where($options['conditions']);
|
||||
}
|
||||
|
@ -73,37 +80,85 @@ class CRUDComponent extends Component
|
|||
}
|
||||
if (isset($options['afterFind'])) {
|
||||
$function = $options['afterFind'];
|
||||
if (is_callable($options['afterFind'])) {
|
||||
$function = $options['afterFind'];
|
||||
$data->each(function($value, $key) use ($function) {
|
||||
if (is_callable($function)) {
|
||||
$data = $data->map(function($value, $key) use ($function) {
|
||||
return $function($value);
|
||||
})->filter(function ($value) {
|
||||
return $value !== false;
|
||||
});
|
||||
} else {
|
||||
$t = $this->Table;
|
||||
$data->each(function($value, $key) use ($t, $function) {
|
||||
$data = $data->map(function($value, $key) use ($t, $function) {
|
||||
return $t->$function($value);
|
||||
})->filter(function ($value) {
|
||||
return $value !== false;
|
||||
});
|
||||
}
|
||||
}
|
||||
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
|
||||
} else {
|
||||
if ($this->metaFieldsSupported()) {
|
||||
$query = $this->includeRequestedMetaFields($query);
|
||||
}
|
||||
$this->Controller->loadComponent('Paginator');
|
||||
$data = $this->Controller->Paginator->paginate($query, $this->Controller->paginate ?? []);
|
||||
if (isset($options['afterFind'])) {
|
||||
$function = $options['afterFind'];
|
||||
if (is_callable($options['afterFind'])) {
|
||||
$function = $options['afterFind'];
|
||||
$data->each(function($value, $key) use ($function) {
|
||||
if (is_callable($function)) {
|
||||
$data = $data->map(function($value, $key) use ($function) {
|
||||
return $function($value);
|
||||
})->filter(function($value) {
|
||||
return $value !== false;
|
||||
});
|
||||
} else {
|
||||
$t = $this->Table;
|
||||
$data->each(function($value, $key) use ($t, $function) {
|
||||
$data = $data->map(function($value, $key) use ($t, $function) {
|
||||
return $t->$function($value);
|
||||
})->filter(function ($value) {
|
||||
return $value !== false;
|
||||
});
|
||||
}
|
||||
}
|
||||
$this->setFilteringContext($options['contextFilters'] ?? [], $params);
|
||||
if ($this->metaFieldsSupported()) {
|
||||
$data = $data->toArray();
|
||||
$metaTemplates = $this->getMetaTemplates()->toArray();
|
||||
foreach ($data as $i => $row) {
|
||||
$data[$i] = $this->attachMetaTemplatesIfNeeded($row, $metaTemplates);
|
||||
}
|
||||
$this->Controller->set('meta_templates', $metaTemplates);
|
||||
}
|
||||
if (true) { // check if stats are requested
|
||||
$modelStatistics = [];
|
||||
if ($this->Table->hasBehavior('Timestamp')) {
|
||||
$modelStatistics = $this->Table->getActivityStatisticsForModel(
|
||||
$this->Table,
|
||||
!is_numeric($this->request->getQuery('statistics_days')) ? 7 : $this->request->getQuery('statistics_days')
|
||||
);
|
||||
}
|
||||
if (!empty($options['statisticsFields'])) {
|
||||
$statIncludeRemaining = $this->request->getQuery('statistics_include_remainging', true);
|
||||
if (is_string($statIncludeRemaining)) {
|
||||
$statIncludeRemaining = $statIncludeRemaining == 'true' ? true : false;
|
||||
}
|
||||
$statIgnoreNull = $this->request->getQuery('statistics_ignore_null', true);
|
||||
if (is_string($statIgnoreNull)) {
|
||||
$statIgnoreNull = $statIgnoreNull == 'true' ? true : false;
|
||||
}
|
||||
$statsOptions = [
|
||||
'limit' => !is_numeric($this->request->getQuery('statistics_entry_amount')) ? 5 : $this->request->getQuery('statistics_entry_amount'),
|
||||
'includeOthers' => $statIncludeRemaining,
|
||||
'ignoreNull' => $statIgnoreNull,
|
||||
];
|
||||
$modelStatistics['usage'] = $this->Table->getStatisticsUsageForModel(
|
||||
$this->Table,
|
||||
$options['statisticsFields'],
|
||||
$statsOptions
|
||||
);
|
||||
}
|
||||
$this->Controller->set('modelStatistics', $modelStatistics);
|
||||
}
|
||||
$this->Controller->set('model', $this->Table);
|
||||
$this->Controller->set('data', $data);
|
||||
}
|
||||
}
|
||||
|
@ -113,8 +168,23 @@ class CRUDComponent extends Component
|
|||
if ($this->taggingSupported()) {
|
||||
$this->Controller->set('taggingEnabled', true);
|
||||
$this->setAllTags();
|
||||
} else {
|
||||
$this->Controller->set('taggingEnabled', false);
|
||||
}
|
||||
if ($this->metaFieldsSupported()) {
|
||||
$metaTemplates = $this->getMetaTemplates()->toArray();
|
||||
$this->Controller->set('metaFieldsEnabled', true);
|
||||
$this->Controller->set('metaTemplates', $metaTemplates);
|
||||
} else {
|
||||
$this->Controller->set('metaFieldsEnabled', false);
|
||||
}
|
||||
$filters = !empty($this->Controller->filterFields) ? $this->Controller->filterFields : [];
|
||||
$typeHandlers = $this->Table->getBehavior('MetaFields')->getTypeHandlers();
|
||||
$typeHandlersOperators = [];
|
||||
foreach ($typeHandlers as $type => $handler) {
|
||||
$typeHandlersOperators[$type] = $handler::OPERATORS;
|
||||
}
|
||||
$this->Controller->set('typeHandlersOperators', $typeHandlersOperators);
|
||||
$this->Controller->set('filters', $filters);
|
||||
$this->Controller->viewBuilder()->setLayout('ajax');
|
||||
$this->Controller->render('/genericTemplates/filters');
|
||||
|
@ -135,28 +205,40 @@ class CRUDComponent extends Component
|
|||
return false;
|
||||
}
|
||||
|
||||
private function getMetaTemplates()
|
||||
private function getMetaTemplates(array $metaTemplateConditions=[])
|
||||
{
|
||||
$metaTemplates = [];
|
||||
if (!empty($this->Table->metaFields)) {
|
||||
$metaQuery = $this->MetaTemplates->find();
|
||||
$metaQuery
|
||||
->order(['is_default' => 'DESC'])
|
||||
->where([
|
||||
'scope' => $this->Table->metaFields,
|
||||
'enabled' => 1
|
||||
]);
|
||||
$metaQuery->contain(['MetaTemplateFields']);
|
||||
$metaTemplates = $metaQuery->all();
|
||||
if (!$this->metaFieldsSupported()) {
|
||||
throw new \Exception(__("Table {$this->TableAlias} does not support meta_fields"));
|
||||
}
|
||||
$this->Controller->set('metaTemplates', $metaTemplates);
|
||||
return true;
|
||||
$metaFieldsBehavior = $this->Table->getBehavior('MetaFields');
|
||||
$metaQuery = $this->MetaTemplates->find();
|
||||
$metaQuery
|
||||
->order(['is_default' => 'DESC'])
|
||||
->where(array_merge(
|
||||
$metaTemplateConditions,
|
||||
['scope' => $metaFieldsBehavior->getScope(), ]
|
||||
))
|
||||
->contain('MetaTemplateFields')
|
||||
->formatResults(function (\Cake\Collection\CollectionInterface $metaTemplates) { // Set meta-template && meta-template-fields indexed by their ID
|
||||
return $metaTemplates
|
||||
->map(function ($metaTemplate) {
|
||||
$metaTemplate->meta_template_fields = Hash::combine($metaTemplate->meta_template_fields, '{n}.id', '{n}');
|
||||
return $metaTemplate;
|
||||
})
|
||||
->indexBy('id');
|
||||
});
|
||||
$metaTemplates = $metaQuery->all();
|
||||
return $metaTemplates;
|
||||
}
|
||||
|
||||
public function add(array $params = []): void
|
||||
{
|
||||
$this->getMetaTemplates();
|
||||
$data = $this->Table->newEmptyEntity();
|
||||
if ($this->metaFieldsSupported()) {
|
||||
$metaTemplates = $this->getMetaTemplates();
|
||||
$data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray());
|
||||
}
|
||||
if ($this->request->is('post')) {
|
||||
$patchEntityParams = [
|
||||
'associated' => [],
|
||||
|
@ -175,6 +257,11 @@ class CRUDComponent extends Component
|
|||
throw new NotFoundException(__('Could not save {0} due to the marshaling failing. Your input is bad and you should feel bad.', $this->ObjectAlias));
|
||||
}
|
||||
}
|
||||
if ($this->metaFieldsSupported()) {
|
||||
$massagedData = $this->massageMetaFields($data, $input, $metaTemplates);
|
||||
unset($input['MetaTemplates']); // Avoid MetaTemplates to be overriden when patching entity
|
||||
$data = $massagedData['entity'];
|
||||
}
|
||||
$data = $this->Table->patchEntity($data, $input, $patchEntityParams);
|
||||
if (isset($params['beforeSave'])) {
|
||||
$data = $params['beforeSave']($data);
|
||||
|
@ -188,9 +275,6 @@ class CRUDComponent extends Component
|
|||
$params['afterSave']($data);
|
||||
}
|
||||
$message = __('{0} added.', $this->ObjectAlias);
|
||||
if (!empty($input['metaFields'])) {
|
||||
$this->saveMetaFields($data->id, $input);
|
||||
}
|
||||
if ($this->Controller->ParamHandler->isRest()) {
|
||||
$this->Controller->restResponsePayload = $this->RestResponse->viewData($savedData, 'json');
|
||||
} else if ($this->Controller->ParamHandler->isAjax()) {
|
||||
|
@ -215,7 +299,7 @@ class CRUDComponent extends Component
|
|||
$message = __(
|
||||
'{0} could not be added.{1}',
|
||||
$this->ObjectAlias,
|
||||
empty($validationMessage) ? '' : PHP_EOL . __('Reason:{0}', $validationMessage)
|
||||
empty($validationMessage) ? '' : PHP_EOL . __('Reason: {0}', $validationMessage)
|
||||
);
|
||||
if ($this->Controller->ParamHandler->isRest()) {
|
||||
$this->Controller->restResponsePayload = $this->RestResponse->viewData($message, 'json');
|
||||
|
@ -255,9 +339,20 @@ class CRUDComponent extends Component
|
|||
foreach ($data->getErrors() as $field => $errorData) {
|
||||
$errorMessages = [];
|
||||
foreach ($errorData as $key => $value) {
|
||||
$errorMessages[] = $value;
|
||||
if (is_array($value)) {
|
||||
$extracted = Hash::extract($value, "{s}.{s}");
|
||||
if (!empty($extracted)) {
|
||||
$errorMessages[] = implode('& ', $extracted);
|
||||
}
|
||||
} else {
|
||||
if (!empty($value)) {
|
||||
$errorMessages[] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($errorMessages)) {
|
||||
$validationMessage .= __('{0}: {1}', $field, implode(',', $errorMessages));
|
||||
}
|
||||
$validationMessage .= __('{0}: {1}', $field, implode(',', $errorMessages));
|
||||
}
|
||||
}
|
||||
return $validationMessage;
|
||||
|
@ -268,6 +363,93 @@ class CRUDComponent extends Component
|
|||
$this->Table->saveMetaFields($id, $input, $this->Table);
|
||||
}
|
||||
|
||||
// prune empty values and marshall fields
|
||||
public function massageMetaFields($entity, $input, $allMetaTemplates=[])
|
||||
{
|
||||
if (empty($input['MetaTemplates']) || !$this->metaFieldsSupported()) {
|
||||
return ['entity' => $entity, 'metafields_to_delete' => []];
|
||||
}
|
||||
|
||||
$metaFieldsTable = TableRegistry::getTableLocator()->get('MetaFields');
|
||||
$metaFieldsIndex = [];
|
||||
if (empty($metaTemplates)) {
|
||||
$allMetaTemplates = $this->getMetaTemplates()->toArray();
|
||||
}
|
||||
if (!empty($entity->meta_fields)) {
|
||||
foreach ($entity->meta_fields as $i => $metaField) {
|
||||
$metaFieldsIndex[$metaField->id] = $i;
|
||||
}
|
||||
} else {
|
||||
$entity->meta_fields = [];
|
||||
}
|
||||
|
||||
$metaFieldsToDelete = [];
|
||||
foreach ($input['MetaTemplates'] as $template_id => $template) {
|
||||
foreach ($template['meta_template_fields'] as $meta_template_field_id => $meta_template_field) {
|
||||
$rawMetaTemplateField = $allMetaTemplates[$template_id]['meta_template_fields'][$meta_template_field_id];
|
||||
foreach ($meta_template_field['metaFields'] as $meta_field_id => $meta_field) {
|
||||
if ($meta_field_id == 'new') { // create new meta_field
|
||||
$new_meta_fields = $meta_field;
|
||||
foreach ($new_meta_fields as $new_value) {
|
||||
if (!empty($new_value)) {
|
||||
$metaField = $metaFieldsTable->newEmptyEntity();
|
||||
$metaFieldsTable->patchEntity($metaField, [
|
||||
'value' => $new_value,
|
||||
'scope' => $this->Table->getBehavior('MetaFields')->getScope(),
|
||||
'field' => $rawMetaTemplateField->field,
|
||||
'meta_template_id' => $rawMetaTemplateField->meta_template_id,
|
||||
'meta_template_field_id' => $rawMetaTemplateField->id,
|
||||
'parent_id' => $entity->id,
|
||||
'uuid' => Text::uuid(),
|
||||
]);
|
||||
$entity->meta_fields[] = $metaField;
|
||||
$entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[] = $metaField;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$new_value = $meta_field['value'];
|
||||
if (!empty($new_value)) { // update meta_field and attach validation errors
|
||||
if (isset($metaFieldsIndex[$meta_field_id])) {
|
||||
$index = $metaFieldsIndex[$meta_field_id];
|
||||
if ($entity->meta_fields[$index]->value != $new_value) { // nothing to do, value hasn't changed
|
||||
$metaFieldsTable->patchEntity($entity->meta_fields[$index], [
|
||||
'value' => $new_value, 'meta_template_field_id' => $rawMetaTemplateField->id
|
||||
], ['value']);
|
||||
$metaFieldsTable->patchEntity(
|
||||
$entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[$meta_field_id],
|
||||
['value' => $new_value, 'meta_template_field_id' => $rawMetaTemplateField->id],
|
||||
['value']
|
||||
);
|
||||
}
|
||||
} else { // metafield comes from a second post where the temporary entity has already been created
|
||||
$metaField = $metaFieldsTable->newEmptyEntity();
|
||||
$metaFieldsTable->patchEntity($metaField, [
|
||||
'value' => $new_value,
|
||||
'scope' => $this->Table->getBehavior('MetaFields')->getScope(), // get scope from behavior
|
||||
'field' => $rawMetaTemplateField->field,
|
||||
'meta_template_id' => $rawMetaTemplateField->meta_template_id,
|
||||
'meta_template_field_id' => $rawMetaTemplateField->id,
|
||||
'parent_id' => $entity->id,
|
||||
'uuid' => Text::uuid(),
|
||||
]);
|
||||
$entity->meta_fields[] = $metaField;
|
||||
$entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[] = $metaField;
|
||||
}
|
||||
} else { // Metafield value is empty, indicating the field should be removed
|
||||
$index = $metaFieldsIndex[$meta_field_id];
|
||||
$metaFieldsToDelete[] = $entity->meta_fields[$index];
|
||||
unset($entity->meta_fields[$index]);
|
||||
unset($entity->MetaTemplates[$template_id]->meta_template_fields[$meta_template_field_id]->metaFields[$meta_field_id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$entity->setDirty('meta_fields', true);
|
||||
return ['entity' => $entity, 'metafields_to_delete' => $metaFieldsToDelete];
|
||||
}
|
||||
|
||||
private function __massageInput($params)
|
||||
{
|
||||
$input = $this->request->getData();
|
||||
|
@ -291,23 +473,38 @@ class CRUDComponent extends Component
|
|||
if (empty($id)) {
|
||||
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
|
||||
}
|
||||
$this->getMetaTemplates();
|
||||
if ($this->taggingSupported()) {
|
||||
$params['contain'][] = 'Tags';
|
||||
$this->setAllTags();
|
||||
}
|
||||
$data = $this->Table->find()->where(['id' => $id]);
|
||||
if (!empty($params['conditions'])) {
|
||||
$data->where($params['conditions']);
|
||||
if ($this->metaFieldsSupported()) {
|
||||
if (empty($params['contain'])) {
|
||||
$params['contain'] = [];
|
||||
}
|
||||
if (is_array($params['contain'])) {
|
||||
$params['contain'][] = 'MetaFields';
|
||||
} else {
|
||||
$params['contain'] = [$params['contain'], 'MetaFields'];
|
||||
}
|
||||
}
|
||||
$data = $data->first();
|
||||
$query = $this->Table->find()->where(['id' => $id]);
|
||||
if (!empty($params['contain'])) {
|
||||
$query->contain($params['contain']);
|
||||
}
|
||||
if (!empty($params['conditions'])) {
|
||||
$query->where($params['conditions']);
|
||||
}
|
||||
$data = $query->first();
|
||||
if (isset($params['afterFind'])) {
|
||||
$data = $params['afterFind']($data, $params);
|
||||
}
|
||||
if (empty($data)) {
|
||||
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
|
||||
}
|
||||
$data = $this->getMetaFields($id, $data);
|
||||
if ($this->metaFieldsSupported()) {
|
||||
$metaTemplates = $this->getMetaTemplates();
|
||||
$data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray());
|
||||
}
|
||||
if ($this->request->is(['post', 'put'])) {
|
||||
$patchEntityParams = [
|
||||
'associated' => []
|
||||
|
@ -322,6 +519,12 @@ class CRUDComponent extends Component
|
|||
throw new NotFoundException(__('Could not save {0} due to the marshaling failing. Your input is bad and you should feel bad.', $this->ObjectAlias));
|
||||
}
|
||||
}
|
||||
if ($this->metaFieldsSupported()) {
|
||||
$massagedData = $this->massageMetaFields($data, $input, $metaTemplates);
|
||||
unset($input['MetaTemplates']); // Avoid MetaTemplates to be overriden when patching entity
|
||||
$data = $massagedData['entity'];
|
||||
$metaFieldsToDelete = $massagedData['metafields_to_delete'];
|
||||
}
|
||||
$data = $this->Table->patchEntity($data, $input, $patchEntityParams);
|
||||
if (isset($params['beforeSave'])) {
|
||||
$data = $params['beforeSave']($data);
|
||||
|
@ -331,14 +534,13 @@ class CRUDComponent extends Component
|
|||
}
|
||||
$savedData = $this->Table->save($data);
|
||||
if ($savedData !== false) {
|
||||
if ($this->metaFieldsSupported() && !empty($metaFieldsToDelete)) {
|
||||
$this->Table->MetaFields->unlink($savedData, $metaFieldsToDelete);
|
||||
}
|
||||
if (isset($params['afterSave'])) {
|
||||
$params['afterSave']($data);
|
||||
}
|
||||
$message = __('{0} `{1}` updated.', $this->ObjectAlias, $savedData->{$this->Table->getDisplayField()});
|
||||
if (!empty($input['metaFields'])) {
|
||||
$this->MetaFields->deleteAll(['scope' => $this->Table->metaFields, 'parent_id' => $savedData->id]);
|
||||
$this->saveMetaFields($savedData->id, $input);
|
||||
}
|
||||
if ($this->Controller->ParamHandler->isRest()) {
|
||||
$this->Controller->restResponsePayload = $this->RestResponse->viewData($savedData, 'json');
|
||||
} else if ($this->Controller->ParamHandler->isAjax()) {
|
||||
|
@ -353,11 +555,11 @@ class CRUDComponent extends Component
|
|||
}
|
||||
} else {
|
||||
$validationErrors = $data->getErrors();
|
||||
$validationMessage = $this->prepareValidationMessage($validationErrors);
|
||||
$validationMessage = $this->prepareValidationError($data);
|
||||
$message = __(
|
||||
'{0} could not be modified.{1}',
|
||||
$this->ObjectAlias,
|
||||
empty($validationMessage) ? '' : PHP_EOL . __('Reason:{0}', $validationMessage)
|
||||
empty($validationMessage) ? '' : PHP_EOL . __('Reason: {0}', $validationMessage)
|
||||
);
|
||||
if ($this->Controller->ParamHandler->isRest()) {
|
||||
} else if ($this->Controller->ParamHandler->isAjax()) {
|
||||
|
@ -376,10 +578,10 @@ class CRUDComponent extends Component
|
|||
|
||||
public function attachMetaData($id, $data)
|
||||
{
|
||||
if (empty($this->Table->metaFields)) {
|
||||
return $data;
|
||||
if (!$this->metaFieldsSupported()) {
|
||||
throw new \Exception(__("Table {$this->TableAlias} does not support meta_fields"));
|
||||
}
|
||||
$metaFieldScope = $this->Table->metaFields;
|
||||
$metaFieldScope = $this->Table->getBehavior('MetaFields')->getScope();
|
||||
$query = $this->MetaTemplates->find()->where(['MetaTemplates.scope' => $metaFieldScope]);
|
||||
$query->contain(['MetaTemplateFields.MetaFields' => function ($q) use ($id, $metaFieldScope) {
|
||||
return $q->where(['MetaFields.scope' => $metaFieldScope, 'MetaFields.parent_id' => $id]);
|
||||
|
@ -408,21 +610,80 @@ class CRUDComponent extends Component
|
|||
return $metaTemplates;
|
||||
}
|
||||
|
||||
public function getMetaFields($id, $data)
|
||||
public function getMetaFields($id)
|
||||
{
|
||||
if (empty($this->Table->metaFields)) {
|
||||
return $data;
|
||||
if (!$this->metaFieldsSupported()) {
|
||||
throw new \Exception(__("Table {$this->TableAlias} does not support meta_fields"));
|
||||
}
|
||||
$query = $this->MetaFields->find();
|
||||
$query->where(['MetaFields.scope' => $this->Table->metaFields, 'MetaFields.parent_id' => $id]);
|
||||
$query->where(['MetaFields.scope' => $this->Table->getBehavior('MetaFields')->getScope(), 'MetaFields.parent_id' => $id]);
|
||||
$metaFields = $query->all();
|
||||
$data['metaFields'] = [];
|
||||
foreach($metaFields as $metaField) {
|
||||
$data['metaFields'][$metaField->meta_template_id][$metaField->field] = $metaField->value;
|
||||
$data = [];
|
||||
foreach ($metaFields as $metaField) {
|
||||
if (empty($data[$metaField->meta_template_id][$metaField->meta_template_field_id])) {
|
||||
$data[$metaField->meta_template_id][$metaField->meta_template_field_id] = [];
|
||||
}
|
||||
$data[$metaField->meta_template_id][$metaField->meta_template_field_id][$metaField->id] = $metaField;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function attachMetaTemplates($data, $metaTemplates, $pruneEmptyDisabled=true)
|
||||
{
|
||||
$this->MetaTemplates = TableRegistry::getTableLocator()->get('MetaTemplates');
|
||||
$metaFields = [];
|
||||
if (!empty($data->id)) {
|
||||
$metaFields = $this->getMetaFields($data->id);
|
||||
}
|
||||
foreach ($metaTemplates as $i => $metaTemplate) {
|
||||
if (isset($metaFields[$metaTemplate->id])) {
|
||||
foreach ($metaTemplate->meta_template_fields as $j => $meta_template_field) {
|
||||
if (isset($metaFields[$metaTemplate->id][$meta_template_field->id])) {
|
||||
$metaTemplates[$metaTemplate->id]->meta_template_fields[$j]['metaFields'] = $metaFields[$metaTemplate->id][$meta_template_field->id];
|
||||
} else {
|
||||
$metaTemplates[$metaTemplate->id]->meta_template_fields[$j]['metaFields'] = [];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!empty($pruneEmptyDisabled) && !$metaTemplate->enabled) {
|
||||
unset($metaTemplates[$i]);
|
||||
}
|
||||
}
|
||||
$newestTemplate = $this->MetaTemplates->getNewestVersion($metaTemplate);
|
||||
if (!empty($newestTemplate) && !empty($metaTemplates[$i])) {
|
||||
$metaTemplates[$i]['hasNewerVersion'] = $newestTemplate;
|
||||
}
|
||||
}
|
||||
$data['MetaTemplates'] = $metaTemplates;
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function includeRequestedMetaFields($query)
|
||||
{
|
||||
$user = $this->Controller->ACL->getUser();
|
||||
$tableSettings = IndexSetting::getTableSetting($user, $this->Table);
|
||||
if (empty($tableSettings['visible_meta_column'])) {
|
||||
return $query;
|
||||
}
|
||||
$containConditions = ['OR' => []];
|
||||
$requestedMetaFields = [];
|
||||
foreach ($tableSettings['visible_meta_column'] as $template_id => $fields) {
|
||||
$containConditions['OR'][] = [
|
||||
'meta_template_id' => $template_id,
|
||||
'meta_template_field_id IN' => array_map('intval', $fields),
|
||||
];
|
||||
foreach ($fields as $field) {
|
||||
$requestedMetaFields[] = ['template_id' => $template_id, 'meta_template_field_id' => intval($field)];
|
||||
}
|
||||
}
|
||||
$this->Controller->set('requestedMetaFields', $requestedMetaFields);
|
||||
return $query->contain([
|
||||
'MetaFields' => [
|
||||
'conditions' => $containConditions
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function view(int $id, array $params = []): void
|
||||
{
|
||||
if (empty($id)) {
|
||||
|
@ -433,12 +694,24 @@ class CRUDComponent extends Component
|
|||
$params['contain'][] = 'Tags';
|
||||
$this->setAllTags();
|
||||
}
|
||||
if ($this->metaFieldsSupported()) {
|
||||
if (!empty($this->request->getQuery('full'))) {
|
||||
$params['contain']['MetaFields'] = ['MetaTemplateFields' => 'MetaTemplates'];
|
||||
} else {
|
||||
$params['contain'][] = 'MetaFields';
|
||||
}
|
||||
}
|
||||
|
||||
$data = $this->Table->get($id, $params);
|
||||
if (empty($data)) {
|
||||
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
|
||||
}
|
||||
$data = $this->attachMetaData($id, $data);
|
||||
if ($this->metaFieldsSupported() && !empty($data['meta_fields'])) {
|
||||
$usedMetaTemplateIDs = array_values(array_unique(Hash::extract($data['meta_fields'], '{n}.meta_template_id')));
|
||||
$data = $this->attachMetaTemplatesIfNeeded($data, null, [
|
||||
'id IN' => $usedMetaTemplateIDs
|
||||
]);
|
||||
}
|
||||
if (isset($params['afterFind'])) {
|
||||
$data = $params['afterFind']($data);
|
||||
}
|
||||
|
@ -451,18 +724,38 @@ class CRUDComponent extends Component
|
|||
$this->Controller->set('entity', $data);
|
||||
}
|
||||
|
||||
public function attachMetaTemplatesIfNeeded($data, array $metaTemplates = null, array $metaTemplateConditions=[])
|
||||
{
|
||||
if (!$this->metaFieldsSupported()) {
|
||||
return $data;
|
||||
}
|
||||
if (!is_null($metaTemplates)) {
|
||||
// We might be in the case where $metaTemplates gets re-used in a while loop
|
||||
// We deep copy the meta-template so that the data attached is not preserved for the next iteration
|
||||
$metaTemplates = array_map(function ($metaTemplate) {
|
||||
$tmpEntity = $this->MetaTemplates->newEntity($metaTemplate->toArray());
|
||||
$tmpEntity['meta_template_fields'] = Hash::combine($tmpEntity['meta_template_fields'], '{n}.id', '{n}'); // newEntity resets array indexing, see https://github.com/cakephp/cakephp/blob/32e3c532fea8abe2db8b697f07dfddf4dfc134ca/src/ORM/Marshaller.php#L369
|
||||
return $tmpEntity;
|
||||
}, $metaTemplates);
|
||||
} else {
|
||||
$metaTemplates = $this->getMetaTemplates($metaTemplateConditions)->toArray();
|
||||
}
|
||||
$data = $this->attachMetaTemplates($data, $metaTemplates);
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function delete($id=false, $params=[]): void
|
||||
{
|
||||
if ($this->request->is('get')) {
|
||||
if(!empty($id)) {
|
||||
$data = $this->Table->find()->where([$this->Table->getAlias() . '.id' => $id]);
|
||||
$query = $this->Table->find()->where([$this->Table->getAlias() . '.id' => $id]);
|
||||
if (!empty($params['conditions'])) {
|
||||
$data->where($params['conditions']);
|
||||
$query->where($params['conditions']);
|
||||
}
|
||||
if (!empty($params['contain'])) {
|
||||
$data->contain($params['contain']);
|
||||
$query->contain($params['contain']);
|
||||
}
|
||||
$data = $data->first();
|
||||
$data = $query->first();
|
||||
if (empty($data)) {
|
||||
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
|
||||
}
|
||||
|
@ -483,18 +776,20 @@ class CRUDComponent extends Component
|
|||
$isBulk = count($ids) > 1;
|
||||
$bulkSuccesses = 0;
|
||||
foreach ($ids as $id) {
|
||||
$skipExecution = false;
|
||||
$data = $this->Table->find()->where([$this->Table->getAlias() . '.id' => $id]);
|
||||
$query = $this->Table->find()->where([$this->Table->getAlias() . '.id' => $id]);
|
||||
if (!empty($params['conditions'])) {
|
||||
$data->where($params['conditions']);
|
||||
$query->where($params['conditions']);
|
||||
}
|
||||
if (!empty($params['contain'])) {
|
||||
$data->contain($params['contain']);
|
||||
$query->contain($params['contain']);
|
||||
}
|
||||
$data = $data->first();
|
||||
$data = $query->first();
|
||||
if (isset($params['beforeSave'])) {
|
||||
try {
|
||||
$data = $params['beforeSave']($data);
|
||||
if ($data === false) {
|
||||
throw new NotFoundException(__('Could not save {0} due to the input failing to meet expectations. Your input is bad and you should feel bad.', $this->ObjectAlias));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$data = false;
|
||||
}
|
||||
|
@ -515,28 +810,32 @@ class CRUDComponent extends Component
|
|||
__('{0} deleted.', $this->ObjectAlias),
|
||||
__('All {0} have been deleted.', Inflector::pluralize($this->ObjectAlias)),
|
||||
__('Could not delete {0}.', $this->ObjectAlias),
|
||||
__('{0} / {1} {2} have been deleted.',
|
||||
__(
|
||||
'{0} / {1} {2} have been deleted.',
|
||||
$bulkSuccesses,
|
||||
count($ids),
|
||||
Inflector::pluralize($this->ObjectAlias)
|
||||
)
|
||||
);
|
||||
$this->setResponseForController('delete', $bulkSuccesses, $message, $data);
|
||||
$additionalData = [];
|
||||
if ($bulkSuccesses > 0) {
|
||||
$additionalData['redirect'] = Router::url(['controller' => $this->Controller->getName(), 'action' => 'index']);
|
||||
}
|
||||
$this->setResponseForController('delete', $bulkSuccesses, $message, $data, null, $additionalData);
|
||||
}
|
||||
$this->Controller->set('metaGroup', 'ContactDB');
|
||||
$this->Controller->set('scope', 'users');
|
||||
$this->Controller->viewBuilder()->setLayout('ajax');
|
||||
$this->Controller->render('/genericTemplates/delete');
|
||||
}
|
||||
|
||||
public function tag($id=false): void
|
||||
public function tag($id = false): void
|
||||
{
|
||||
if (!$this->taggingSupported()) {
|
||||
throw new Exception("Table {$this->TableAlias} does not support tagging");
|
||||
}
|
||||
if ($this->request->is('get')) {
|
||||
$this->setAllTags();
|
||||
if(!empty($id)) {
|
||||
if (!empty($id)) {
|
||||
$params = [
|
||||
'contain' => 'Tags',
|
||||
];
|
||||
|
@ -576,7 +875,8 @@ class CRUDComponent extends Component
|
|||
__('{0} tagged with `{1}`.', $this->ObjectAlias, $input['tag_list']),
|
||||
__('All {0} have been tagged.', Inflector::pluralize($this->ObjectAlias)),
|
||||
__('Could not tag {0} with `{1}`.', $this->ObjectAlias, $input['tag_list']),
|
||||
__('{0} / {1} {2} have been tagged.',
|
||||
__(
|
||||
'{0} / {1} {2} have been tagged.',
|
||||
$bulkSuccesses,
|
||||
count($ids),
|
||||
Inflector::pluralize($this->ObjectAlias)
|
||||
|
@ -588,14 +888,14 @@ class CRUDComponent extends Component
|
|||
$this->Controller->render('/genericTemplates/tagForm');
|
||||
}
|
||||
|
||||
public function untag($id=false): void
|
||||
public function untag($id = false): void
|
||||
{
|
||||
if (!$this->taggingSupported()) {
|
||||
throw new Exception("Table {$this->TableAlias} does not support tagging");
|
||||
}
|
||||
if ($this->request->is('get')) {
|
||||
$this->setAllTags();
|
||||
if(!empty($id)) {
|
||||
if (!empty($id)) {
|
||||
$params = [
|
||||
'contain' => 'Tags',
|
||||
];
|
||||
|
@ -637,7 +937,8 @@ class CRUDComponent extends Component
|
|||
__('{0} untagged with `{1}`.', $this->ObjectAlias, implode(', ', $tagsToRemove)),
|
||||
__('All {0} have been untagged.', Inflector::pluralize($this->ObjectAlias)),
|
||||
__('Could not untag {0} with `{1}`.', $this->ObjectAlias, $input['tag_list']),
|
||||
__('{0} / {1} {2} have been untagged.',
|
||||
__(
|
||||
'{0} / {1} {2} have been untagged.',
|
||||
$bulkSuccesses,
|
||||
count($ids),
|
||||
Inflector::pluralize($this->ObjectAlias)
|
||||
|
@ -672,13 +973,16 @@ class CRUDComponent extends Component
|
|||
$this->Controller->render('/genericTemplates/tag');
|
||||
}
|
||||
|
||||
public function setResponseForController($action, $success, $message, $data=[], $errors=null)
|
||||
public function setResponseForController($action, $success, $message, $data = [], $errors = null, $additionalData = [])
|
||||
{
|
||||
if ($success) {
|
||||
if ($this->Controller->ParamHandler->isRest()) {
|
||||
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
|
||||
} elseif ($this->Controller->ParamHandler->isAjax()) {
|
||||
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($this->ObjectAlias, $action, $data, $message);
|
||||
if (!empty($additionalData['redirect'])) { // If a redirection occurs, we need to make sure the flash message gets displayed
|
||||
$this->Controller->Flash->success($message);
|
||||
}
|
||||
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($this->ObjectAlias, $action, $data, $message, $additionalData);
|
||||
} else {
|
||||
$this->Controller->Flash->success($message);
|
||||
$this->Controller->redirect($this->Controller->referer());
|
||||
|
@ -687,6 +991,9 @@ class CRUDComponent extends Component
|
|||
if ($this->Controller->ParamHandler->isRest()) {
|
||||
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
|
||||
} elseif ($this->Controller->ParamHandler->isAjax()) {
|
||||
if (!empty($additionalData['redirect'])) { // If a redirection occurs, we need to make sure the flash message gets displayed
|
||||
$this->Controller->Flash->error($message);
|
||||
}
|
||||
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($this->ObjectAlias, $action, $data, $message, !is_null($errors) ? $errors : $data->getErrors());
|
||||
} else {
|
||||
$this->Controller->Flash->error($message);
|
||||
|
@ -712,7 +1019,7 @@ class CRUDComponent extends Component
|
|||
* @return Array The ID converted to a list or the list of provided IDs from the request
|
||||
* @throws NotFoundException when no ID could be found
|
||||
*/
|
||||
public function getIdsOrFail($id=false): Array
|
||||
public function getIdsOrFail($id = false): array
|
||||
{
|
||||
$params = $this->Controller->ParamHandler->harvestParams(['ids']);
|
||||
if (!empty($params['ids'])) {
|
||||
|
@ -763,22 +1070,27 @@ class CRUDComponent extends Component
|
|||
return $massagedFilters;
|
||||
}
|
||||
|
||||
public function setQuickFilters(array $params, \Cake\ORM\Query $query, array $quickFilterFields): \Cake\ORM\Query
|
||||
public function setQuickFilters(array $params, \Cake\ORM\Query $query, array $options): \Cake\ORM\Query
|
||||
{
|
||||
$quickFilterFields = $options['quickFilters'];
|
||||
$queryConditions = [];
|
||||
$this->Controller->set('quickFilter', empty($quickFilterFields) ? [] : $quickFilterFields);
|
||||
if ($this->metaFieldsSupported() && !empty($options['quickFilterForMetaField']['enabled'])) {
|
||||
$this->Controller->set('quickFilterForMetaField', [
|
||||
'enabled' => $options['quickFilterForMetaField']['enabled'] ?? false,
|
||||
'wildcard_search' => $options['quickFilterForMetaField']['enabled'] ?? false,
|
||||
]);
|
||||
}
|
||||
if (!empty($params['quickFilter']) && !empty($quickFilterFields)) {
|
||||
$this->Controller->set('quickFilterValue', $params['quickFilter']);
|
||||
foreach ($quickFilterFields as $filterField) {
|
||||
$likeCondition = false;
|
||||
if (is_array($filterField)) {
|
||||
$likeCondition = reset($filterField);
|
||||
$filterFieldName = array_key_first($filterField);
|
||||
$queryConditions[$filterFieldName . ' LIKE'] = '%' . $params['quickFilter'] .'%';
|
||||
} else {
|
||||
$queryConditions[$filterField] = $params['quickFilter'];
|
||||
}
|
||||
$queryConditions = $this->genQuickFilterConditions($params, $quickFilterFields);
|
||||
|
||||
if ($this->metaFieldsSupported() && !empty($options['quickFilterForMetaField']['enabled'])) {
|
||||
$searchValue = !empty($options['quickFilterForMetaField']['wildcard_search']) ? "%{$params['quickFilter']}%" : $params['quickFilter'];
|
||||
$metaFieldConditions = $this->Table->buildMetaFieldQuerySnippetForMatchingParent(['value' => $searchValue]);
|
||||
$queryConditions[] = $metaFieldConditions;
|
||||
}
|
||||
|
||||
$query->where(['OR' => $queryConditions]);
|
||||
} else {
|
||||
$this->Controller->set('quickFilterValue', '');
|
||||
|
@ -786,6 +1098,25 @@ class CRUDComponent extends Component
|
|||
return $query;
|
||||
}
|
||||
|
||||
public function genQuickFilterConditions(array $params, array $quickFilterFields): array
|
||||
{
|
||||
$queryConditions = [];
|
||||
foreach ($quickFilterFields as $filterField) {
|
||||
if (is_array($filterField)) {
|
||||
reset($filterField);
|
||||
$filterFieldName = array_key_first($filterField);
|
||||
if (!empty($filterField[$filterFieldName])) {
|
||||
$queryConditions[$filterFieldName . ' LIKE'] = '%' . $params['quickFilter'] . '%';
|
||||
} else {
|
||||
$queryConditions[$filterField] = $params['quickFilter'];
|
||||
}
|
||||
} else {
|
||||
$queryConditions[$filterField] = $params['quickFilter'];
|
||||
}
|
||||
}
|
||||
return $queryConditions;
|
||||
}
|
||||
|
||||
protected function setFilters($params, \Cake\ORM\Query $query, array $options): \Cake\ORM\Query
|
||||
{
|
||||
$filteringLabel = !empty($params['filteringLabel']) ? $params['filteringLabel'] : '';
|
||||
|
@ -793,7 +1124,7 @@ class CRUDComponent extends Component
|
|||
$filteringTags = !empty($params['filteringTags']) && $this->taggingSupported() ? $params['filteringTags'] : '';
|
||||
unset($params['filteringTags']);
|
||||
$customFilteringFunction = '';
|
||||
$chosenFilter = '';
|
||||
$chosenFilter = [];
|
||||
if (!empty($options['contextFilters']['custom'])) {
|
||||
foreach ($options['contextFilters']['custom'] as $filter) {
|
||||
if ($filter['label'] == $filteringLabel) {
|
||||
|
@ -816,10 +1147,10 @@ class CRUDComponent extends Component
|
|||
}
|
||||
if (!empty($params['simpleFilters'])) {
|
||||
foreach ($params['simpleFilters'] as $filter => $filterValue) {
|
||||
$activeFilters[$filter] = $filterValue;
|
||||
if ($filter === 'quickFilter') {
|
||||
continue;
|
||||
}
|
||||
$activeFilters[$filter] = $filterValue;
|
||||
if (is_array($filterValue)) {
|
||||
$query->where([($filter . ' IN') => $filterValue]);
|
||||
} else {
|
||||
|
@ -841,20 +1172,39 @@ class CRUDComponent extends Component
|
|||
$query = $this->setTagFilters($query, $filteringTags);
|
||||
}
|
||||
|
||||
if ($this->metaFieldsSupported()) {
|
||||
$filteringMetaFields = $this->getMetaFieldFiltersFromQuery();
|
||||
if (!empty($filteringMetaFields)) {
|
||||
$activeFilters['filteringMetaFields'] = $filteringMetaFields;
|
||||
}
|
||||
$query = $this->setMetaFieldFilters($query, $filteringMetaFields);
|
||||
}
|
||||
|
||||
$this->Controller->set('activeFilters', $activeFilters);
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function setMetaFieldFilters($query, $metaFieldFilters)
|
||||
{
|
||||
$metaFieldConditions = $this->Table->buildMetaFieldQuerySnippetForMatchingParent($metaFieldFilters);
|
||||
$query->where($metaFieldConditions);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function setTagFilters($query, $tags)
|
||||
{
|
||||
$modelAlias = $this->Table->getAlias();
|
||||
$subQuery = $this->Table->find('tagged', [
|
||||
'name' => $tags,
|
||||
'forceAnd' => true
|
||||
'OperatorAND' => true
|
||||
])->select($modelAlias . '.id');
|
||||
return $query->where([$modelAlias . '.id IN' => $subQuery]);
|
||||
}
|
||||
|
||||
// FIXME: Adding related condition with association having `through` setup might include duplicate in the result set
|
||||
// We should probably rely on `innerJoinWith` and perform deduplication via `distinct`
|
||||
// Or grouping by primary key for the main model (however this is not optimal/efficient/clean)
|
||||
protected function setNestedRelatedCondition($query, $filterParts, $filterValue)
|
||||
{
|
||||
$modelName = $filterParts[0];
|
||||
|
@ -863,7 +1213,7 @@ class CRUDComponent extends Component
|
|||
$query = $this->setRelatedCondition($query, $modelName, $fieldName, $filterValue);
|
||||
} else {
|
||||
$filterParts = array_slice($filterParts, 1);
|
||||
$query = $query->matching($modelName, function(\Cake\ORM\Query $q) use ($filterParts, $filterValue) {
|
||||
$query = $query->matching($modelName, function (\Cake\ORM\Query $q) use ($filterParts, $filterValue) {
|
||||
return $this->setNestedRelatedCondition($q, $filterParts, $filterValue);
|
||||
});
|
||||
}
|
||||
|
@ -872,7 +1222,7 @@ class CRUDComponent extends Component
|
|||
|
||||
protected function setRelatedCondition($query, $modelName, $fieldName, $filterValue)
|
||||
{
|
||||
return $query->matching($modelName, function(\Cake\ORM\Query $q) use ($fieldName, $filterValue) {
|
||||
return $query->matching($modelName, function (\Cake\ORM\Query $q) use ($fieldName, $filterValue) {
|
||||
return $this->setValueCondition($q, $fieldName, $filterValue);
|
||||
});
|
||||
}
|
||||
|
@ -891,8 +1241,14 @@ class CRUDComponent extends Component
|
|||
protected function setFilteringContext($contextFilters, $params)
|
||||
{
|
||||
$filteringContexts = [];
|
||||
if (!isset($contextFilters['allow_all']) || $contextFilters['allow_all']) {
|
||||
$filteringContexts[] = ['label' => __('All')];
|
||||
if (
|
||||
!isset($contextFilters['_all']) ||
|
||||
!isset($contextFilters['_all']['enabled']) ||
|
||||
!empty($contextFilters['_all']['enabled'])
|
||||
) {
|
||||
$filteringContexts[] = [
|
||||
'label' => !empty($contextFilters['_all']['text']) ? h($contextFilters['_all']['text']) : __('All')
|
||||
];
|
||||
}
|
||||
if (!empty($contextFilters['fields'])) {
|
||||
foreach ($contextFilters['fields'] as $field) {
|
||||
|
@ -918,6 +1274,28 @@ class CRUDComponent extends Component
|
|||
$this->Controller->set('filteringContexts', $filteringContexts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fake filtering label set to the filter to be used by default if the request does not supply one
|
||||
* This fake filtering label will then be used to set approriate filters on the query
|
||||
*
|
||||
* @param array $options CRUD options
|
||||
* @param array $params Collected params from the request
|
||||
* @return array
|
||||
*/
|
||||
protected function fakeContextFilter($options, $params): array
|
||||
{
|
||||
if (empty($params['filteringLabel']) && !empty($options['contextFilters']['custom'])) {
|
||||
foreach ($options['contextFilters']['custom'] as $contextFilter) {
|
||||
if (!empty($contextFilter['default'])) {
|
||||
$params['filteringLabel'] = $contextFilter['label'];
|
||||
$this->Controller->set('fakeFilteringLabel', $contextFilter['label']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function setParentConditionsForMetaFields($query, array $metaConditions)
|
||||
{
|
||||
$metaTemplates = $this->MetaFields->MetaTemplates->find('list', [
|
||||
|
@ -942,7 +1320,7 @@ class CRUDComponent extends Component
|
|||
throw new Exception('Invalid passed conditions');
|
||||
}
|
||||
foreach ($metaANDConditions as $i => $conditions) {
|
||||
$metaANDConditions[$i]['scope'] = $this->Table->metaFields;
|
||||
$metaANDConditions[$i]['scope'] = $this->Table->getBehavior('MetaFields')->getScope();
|
||||
}
|
||||
$firstCondition = $this->prefixConditions('MetaFields', $metaANDConditions[0]);
|
||||
$conditionsToJoin = array_slice($metaANDConditions, 1);
|
||||
|
@ -974,6 +1352,11 @@ class CRUDComponent extends Component
|
|||
return $this->Table->behaviors()->has('Tag');
|
||||
}
|
||||
|
||||
public function metaFieldsSupported()
|
||||
{
|
||||
return $this->Table->hasBehavior('MetaFields');
|
||||
}
|
||||
|
||||
public function setAllTags()
|
||||
{
|
||||
$this->Tags = TableRegistry::getTableLocator()->get('Tags.Tags');
|
||||
|
@ -999,7 +1382,8 @@ class CRUDComponent extends Component
|
|||
}
|
||||
$savedData = $this->Table->save($data);
|
||||
if ($savedData !== false) {
|
||||
$message = __('{0} field {1}. (ID: {2} {3})',
|
||||
$message = __(
|
||||
'{0} field {1}. (ID: {2} {3})',
|
||||
$fieldName,
|
||||
$data->{$fieldName} ? __('enabled') : __('disabled'),
|
||||
Inflector::humanize($this->ObjectAlias),
|
||||
|
@ -1023,7 +1407,7 @@ class CRUDComponent extends Component
|
|||
$message = __(
|
||||
'{0} could not be modified.{1}',
|
||||
$this->ObjectAlias,
|
||||
empty($validationMessage) ? '' : ' ' . __('Reason:{0}', $validationMessage)
|
||||
empty($validationMessage) ? '' : ' ' . __('Reason: {0}', $validationMessage)
|
||||
);
|
||||
if ($this->Controller->ParamHandler->isRest()) {
|
||||
} else if ($this->Controller->ParamHandler->isAjax()) {
|
||||
|
@ -1081,9 +1465,9 @@ class CRUDComponent extends Component
|
|||
[$this->Table->getAlias() => $this->Table->getTable()],
|
||||
[sprintf('%s.id = %s.%s', $this->Table->getAlias(), $associatedTable->getAlias(), $association->getForeignKey())]
|
||||
)
|
||||
->where([
|
||||
["${field} IS NOT" => NULL]
|
||||
]);
|
||||
->where([
|
||||
["${field} IS NOT" => NULL]
|
||||
]);
|
||||
} else if ($associationType == 'manyToOne') {
|
||||
$fieldToExtract = sprintf('%s.%s', Inflector::singularize(strtolower($model)), $subField);
|
||||
$query = $this->Table->find()->contain($model);
|
||||
|
@ -1100,6 +1484,25 @@ class CRUDComponent extends Component
|
|||
->toList();
|
||||
}
|
||||
|
||||
private function getMetaFieldFiltersFromQuery(): array
|
||||
{
|
||||
$filters = [];
|
||||
foreach ($this->request->getQueryParams() as $filterName => $value) {
|
||||
$prefix = '_metafield';
|
||||
if (substr($filterName, 0, strlen($prefix)) === $prefix) {
|
||||
$dissected = explode('_', substr($filterName, strlen($prefix)));
|
||||
if (count($dissected) == 3) { // Skip if template_id or template_field_id not provided
|
||||
$filters[] = [
|
||||
'meta_template_id' => intval($dissected[1]),
|
||||
'meta_template_field_id' => intval($dissected[2]),
|
||||
'value' => $value,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $filters;
|
||||
}
|
||||
|
||||
private function renderViewInVariable($templateRelativeName, $data)
|
||||
{
|
||||
$builder = new ViewBuilder();
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
namespace BreadcrumbNavigation;
|
||||
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
|
||||
|
||||
class ApiNavigation extends BaseNavigation
|
||||
{
|
||||
function addRoutes()
|
||||
{
|
||||
$this->bcf->addRoute('Api', 'index', [
|
||||
'label' => __('API'),
|
||||
'url' => '/api/index',
|
||||
'icon' => 'code'
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -12,21 +12,52 @@ class MetaTemplatesNavigation extends BaseNavigation
|
|||
$this->bcf->addRoute('MetaTemplates', 'view', $this->bcf->defaultCRUD('MetaTemplates', 'view'));
|
||||
$this->bcf->addRoute('MetaTemplates', 'enable', [
|
||||
'label' => __('Enable'),
|
||||
'icon' => 'check',
|
||||
'icon' => 'check-square',
|
||||
'url' => '/metaTemplates/enable/{{id}}/enabled',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
]);
|
||||
$this->bcf->addRoute('MetaTemplates', 'set_default', [
|
||||
'label' => __('Set as default'),
|
||||
'icon' => 'check',
|
||||
'icon' => 'check-square',
|
||||
'url' => '/metaTemplates/toggle/{{id}}/default',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
]);
|
||||
|
||||
$totalUpdateCount = 0;
|
||||
if (!empty($this->viewVars['updateableTemplates']['automatically-updateable']) && !empty($this->viewVars['updateableTemplates']['new'])) {
|
||||
$udpateCount = count($this->viewVars['updateableTemplates']['automatically-updateable']) ?? 0;
|
||||
$newCount = count($this->viewVars['updateableTemplates']['new']) ?? 0;
|
||||
$totalUpdateCount = $udpateCount + $newCount;
|
||||
}
|
||||
$updateRouteConfig = [
|
||||
'label' => __('Update all templates'),
|
||||
'icon' => 'download',
|
||||
'url' => '/metaTemplates/updateAllTemplates',
|
||||
];
|
||||
if ($totalUpdateCount > 0) {
|
||||
$updateRouteConfig['badge'] = [
|
||||
'text' => h($totalUpdateCount),
|
||||
'variant' => 'warning',
|
||||
'title' => __('There are {0} new meta-template(s) and {1} update(s) available', h($newCount), h($udpateCount)),
|
||||
];
|
||||
}
|
||||
$this->bcf->addRoute('MetaTemplates', 'update_all_templates', $updateRouteConfig);
|
||||
$this->bcf->addRoute('MetaTemplates', 'update', [
|
||||
'label' => __('Update template'),
|
||||
'icon' => 'download',
|
||||
'url' => '/metaTemplates/update',
|
||||
]);
|
||||
$this->bcf->addRoute('MetaTemplates', 'prune_outdated_template', [
|
||||
'label' => __('Prune outdated template'),
|
||||
'icon' => 'trash',
|
||||
'url' => '/metaTemplates/prune_outdated_template',
|
||||
]);
|
||||
}
|
||||
|
||||
public function addParents()
|
||||
{
|
||||
$this->bcf->addParent('MetaTemplates', 'view', 'MetaTemplates', 'index');
|
||||
$this->bcf->addParent('MetaTemplates', 'update', 'MetaTemplates', 'index');
|
||||
}
|
||||
|
||||
public function addLinks()
|
||||
|
@ -36,6 +67,42 @@ class MetaTemplatesNavigation extends BaseNavigation
|
|||
|
||||
public function addActions()
|
||||
{
|
||||
$totalUpdateCount = 0;
|
||||
if (!empty($this->viewVars['updateableTemplates']['not-up-to-date']) && !empty($this->viewVars['updateableTemplates']['new'])) {
|
||||
$udpateCount = count($this->viewVars['updateableTemplates']['not-up-to-date']) ?? 0;
|
||||
$newCount = count($this->viewVars['updateableTemplates']['new']) ?? 0;
|
||||
$totalUpdateCount = $udpateCount + $newCount;
|
||||
}
|
||||
$updateAllActionConfig = [
|
||||
'label' => __('Update template'),
|
||||
'url' => '/metaTemplates/updateAllTemplates',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
];
|
||||
if ($totalUpdateCount > 0) {
|
||||
$updateAllActionConfig['badge'] = [
|
||||
'text' => h($totalUpdateCount),
|
||||
'variant' => 'warning',
|
||||
'title' => __('There are {0} new meta-template(s) and {1} update(s) available', h($newCount), h($udpateCount)),
|
||||
];
|
||||
}
|
||||
$this->bcf->addAction('MetaTemplates', 'index', 'MetaTemplates', 'update_all_templates', $updateAllActionConfig);
|
||||
$this->bcf->addAction('MetaTemplates', 'index', 'MetaTemplates', 'prune_outdated_template', [
|
||||
'label' => __('Prune outdated template'),
|
||||
'url' => '/metaTemplates/prune_outdated_template',
|
||||
]);
|
||||
|
||||
if (empty($this->viewVars['updateableTemplates']['up-to-date'])) {
|
||||
$this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'update', [
|
||||
'label' => __('Update template'),
|
||||
'url' => '/metaTemplates/update/{{id}}',
|
||||
'url_vars' => ['id' => 'id'],
|
||||
'variant' => 'warning',
|
||||
'badge' => [
|
||||
'variant' => 'warning',
|
||||
'title' => __('Update available')
|
||||
]
|
||||
]);
|
||||
}
|
||||
$this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'enable');
|
||||
$this->bcf->addAction('MetaTemplates', 'view', 'MetaTemplates', 'set_default');
|
||||
}
|
||||
|
|
|
@ -5,12 +5,14 @@ class BaseNavigation
|
|||
{
|
||||
protected $bcf;
|
||||
protected $request;
|
||||
protected $viewVars;
|
||||
public $currentUser;
|
||||
|
||||
public function __construct($bcf, $request)
|
||||
public function __construct($bcf, $request, $viewVars)
|
||||
{
|
||||
$this->bcf = $bcf;
|
||||
$this->request = $request;
|
||||
$this->viewVars = $viewVars;
|
||||
}
|
||||
|
||||
public function setCurrentUser($currentUser)
|
||||
|
|
|
@ -38,6 +38,11 @@ class Sidemenu {
|
|||
'label' => __('Sharing Groups'),
|
||||
'icon' => $this->iconTable['SharingGroups'],
|
||||
'url' => '/sharingGroups/index',
|
||||
],
|
||||
'MailingLists' => [
|
||||
'label' => __('Mailing Lists'),
|
||||
'icon' => $this->iconTable['MailingLists'],
|
||||
'url' => '/mailingLists/index',
|
||||
]
|
||||
],
|
||||
__('Synchronisation') => [
|
||||
|
@ -46,11 +51,6 @@ class Sidemenu {
|
|||
'icon' => $this->iconTable['Broods'],
|
||||
'url' => '/broods/index',
|
||||
],
|
||||
'API' => [
|
||||
'label' => __('API'),
|
||||
'icon' => $this->iconTable['API'],
|
||||
'url' => '/api/index',
|
||||
],
|
||||
],
|
||||
__('Administration') => [
|
||||
'Roles' => [
|
||||
|
@ -78,13 +78,13 @@ class Sidemenu {
|
|||
'icon' => $this->iconTable['Inbox'],
|
||||
'url' => '/inbox/index',
|
||||
'children' => [
|
||||
'index' => [
|
||||
'inbox' => [
|
||||
'url' => '/inbox/index',
|
||||
'label' => __('Inbox')
|
||||
'label' => __('Inbox'),
|
||||
],
|
||||
'outbox' => [
|
||||
'url' => '/outbox/index',
|
||||
'label' => __('Outbox')
|
||||
'label' => __('Outbox'),
|
||||
],
|
||||
]
|
||||
],
|
||||
|
@ -125,6 +125,11 @@ class Sidemenu {
|
|||
],
|
||||
]
|
||||
],
|
||||
'API' => [
|
||||
'label' => __('API'),
|
||||
'icon' => $this->iconTable['API'],
|
||||
'url' => '/api/index',
|
||||
],
|
||||
],
|
||||
'Open' => [
|
||||
'Organisations' => [
|
||||
|
|
|
@ -25,6 +25,7 @@ class NavigationComponent extends Component
|
|||
'Organisations' => 'building',
|
||||
'EncryptionKeys' => 'key',
|
||||
'SharingGroups' => 'user-friends',
|
||||
'MailingLists' => 'mail-bulk',
|
||||
'Broods' => 'network-wired',
|
||||
'Roles' => 'id-badge',
|
||||
'Users' => 'users',
|
||||
|
@ -43,10 +44,9 @@ class NavigationComponent extends Component
|
|||
$this->request = $config['request'];
|
||||
}
|
||||
|
||||
public function genBreadcrumbs(\App\Model\Entity\User $user)
|
||||
public function beforeRender($event)
|
||||
{
|
||||
$this->currentUser = $user;
|
||||
$this->breadcrumb = $this->fullBreadcrumb = $this->genBreadcrumb();
|
||||
$this->fullBreadcrumb = $this->genBreadcrumb();
|
||||
}
|
||||
|
||||
public function getSideMenu(): array
|
||||
|
@ -141,7 +141,8 @@ class NavigationComponent extends Component
|
|||
$navigationClassname = str_replace('.php', '', $navigationFile);
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . $navigationFile);
|
||||
$reflection = new \ReflectionClass("BreadcrumbNavigation\\{$navigationClassname}Navigation");
|
||||
$navigationClasses[$navigationClassname] = $reflection->newInstance($bcf, $request);
|
||||
$viewVars = $this->_registry->getController()->viewBuilder()->getVars();
|
||||
$navigationClasses[$navigationClassname] = $reflection->newInstance($bcf, $request, $viewVars);
|
||||
$navigationClasses[$navigationClassname]->setCurrentUser($this->currentUser);
|
||||
}
|
||||
return $navigationClasses;
|
||||
|
@ -161,6 +162,7 @@ class NavigationComponent extends Component
|
|||
'Tags',
|
||||
'LocalTools',
|
||||
'UserSettings',
|
||||
'MailingLists',
|
||||
];
|
||||
foreach ($CRUDControllers as $controller) {
|
||||
$bcf->setDefaultCRUDForModel($controller);
|
||||
|
@ -250,6 +252,7 @@ class BreadcrumbFactory
|
|||
$routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'icon');
|
||||
$routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'label');
|
||||
$routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'textGetter');
|
||||
$routeConfig = $this->addIfNotEmpty($routeConfig, $config, 'badge');
|
||||
return $routeConfig;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller\Component;
|
||||
|
||||
use Cake\Controller\Component;
|
||||
use Cake\Core\Configure;
|
||||
use Cake\Http\Exception\MethodNotAllowedException;
|
||||
use Cake\ORM\TableRegistry;
|
||||
|
||||
class NotificationComponent extends Component
|
||||
{
|
||||
private $tables = [
|
||||
'Inbox',
|
||||
];
|
||||
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
$this->request = $config['request'];
|
||||
$this->Controller = $this->getController();
|
||||
}
|
||||
|
||||
public function getNotifications(): array
|
||||
{
|
||||
$notifications = [];
|
||||
$notifications = $this->collectNotificationsFromTables();
|
||||
return $notifications;
|
||||
}
|
||||
|
||||
private function collectNotificationsFromTables(): array
|
||||
{
|
||||
$notifications = [];
|
||||
foreach ($this->tables as $tableName) {
|
||||
$table = TableRegistry::getTableLocator()->get($tableName);
|
||||
$tableNotifications = $this->collectNotificationFromTable($table);
|
||||
$notifications = array_merge($notifications, $tableNotifications);
|
||||
}
|
||||
return $notifications;
|
||||
}
|
||||
|
||||
private function collectNotificationFromTable($table): array
|
||||
{
|
||||
$notifications = [];
|
||||
if (method_exists($table, 'collectNotifications')) {
|
||||
$notifications = $table->collectNotifications($this->Controller->ACL->getUser());
|
||||
}
|
||||
return $notifications;
|
||||
}
|
||||
}
|
|
@ -27,15 +27,15 @@ class ParamHandlerComponent extends Component
|
|||
$queryString = str_replace('.', '_', $filter);
|
||||
$queryString = str_replace(' ', '_', $queryString);
|
||||
if ($this->request->getQuery($queryString) !== null) {
|
||||
$parsedParams[$filter] = $this->request->getQuery($queryString);
|
||||
continue;
|
||||
}
|
||||
if (($this->request->getQuery($filter)) !== null) {
|
||||
$parsedParams[$filter] = $this->request->getQuery($filter);
|
||||
if (is_array($this->request->getQuery($queryString))) {
|
||||
$parsedParams[$filter] = array_map('trim', $this->request->getQuery($queryString));
|
||||
} else {
|
||||
$parsedParams[$filter] = trim($this->request->getQuery($queryString));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (($this->request->is('post') || $this->request->is('put')) && $this->request->getData($filter) !== null) {
|
||||
$parsedParams[$filter] = $this->request->getData($filter);
|
||||
$parsedParams[$filter] = trim($this->request->getData($filter));
|
||||
}
|
||||
}
|
||||
return $parsedParams;
|
||||
|
|
|
@ -17,6 +17,7 @@ class EncryptionKeysController extends AppController
|
|||
public $filterFields = ['owner_model', 'owner_id', 'encryption_key'];
|
||||
public $quickFilterFields = ['encryption_key'];
|
||||
public $containFields = ['Individuals', 'Organisations'];
|
||||
public $statisticsFields = ['type'];
|
||||
|
||||
public function index()
|
||||
{
|
||||
|
@ -28,7 +29,8 @@ class EncryptionKeysController extends AppController
|
|||
'type'
|
||||
]
|
||||
],
|
||||
'contain' => $this->containFields
|
||||
'contain' => $this->containFields,
|
||||
'statisticsFields' => $this->statisticsFields,
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
|
|
|
@ -16,25 +16,22 @@ class IndividualsController extends AppController
|
|||
public $quickFilterFields = ['uuid', ['email' => true], ['first_name' => true], ['last_name' => true], 'position'];
|
||||
public $filterFields = ['uuid', 'email', 'first_name', 'last_name', 'position', 'Organisations.id', 'Alignments.type'];
|
||||
public $containFields = ['Alignments' => 'Organisations'];
|
||||
public $statisticsFields = ['position'];
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->CRUD->index([
|
||||
'filters' => $this->filterFields,
|
||||
'quickFilters' => $this->quickFilterFields,
|
||||
'contextFilters' => [
|
||||
'fields' => [
|
||||
'Alignments.type'
|
||||
]
|
||||
],
|
||||
'contain' => $this->containFields
|
||||
'quickFilterForMetaField' => ['enabled' => true, 'wildcard_search' => true],
|
||||
'contain' => $this->containFields,
|
||||
'statisticsFields' => $this->statisticsFields,
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('alignmentScope', 'individuals');
|
||||
$this->set('metaGroup', 'ContactDB');
|
||||
}
|
||||
|
||||
public function filtering()
|
||||
|
@ -49,7 +46,6 @@ class IndividualsController extends AppController
|
|||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'ContactDB');
|
||||
}
|
||||
|
||||
public function view($id)
|
||||
|
@ -59,7 +55,6 @@ class IndividualsController extends AppController
|
|||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'ContactDB');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
|
@ -69,7 +64,6 @@ class IndividualsController extends AppController
|
|||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'ContactDB');
|
||||
$this->render('add');
|
||||
}
|
||||
|
||||
|
@ -80,7 +74,6 @@ class IndividualsController extends AppController
|
|||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'ContactDB');
|
||||
}
|
||||
|
||||
public function tag($id)
|
||||
|
|
|
@ -21,7 +21,6 @@ class InstanceController extends AppController
|
|||
|
||||
public function home()
|
||||
{
|
||||
// $this->set('md', file_get_contents(ROOT . '/README.md'));
|
||||
$statistics = $this->Instance->getStatistics();
|
||||
$this->set('statistics', $statistics);
|
||||
}
|
||||
|
@ -44,7 +43,7 @@ class InstanceController extends AppController
|
|||
}
|
||||
$data = [];
|
||||
if (!empty($searchValue)) {
|
||||
$data = $this->Instance->searchAll($searchValue, $limit, $model);
|
||||
$data = $this->Instance->searchAll($searchValue, $this->ACL->getUser(), $limit, $model);
|
||||
}
|
||||
if ($this->ParamHandler->isRest()) {
|
||||
return $this->RestResponse->viewData($data, 'json');
|
||||
|
|
|
@ -0,0 +1,358 @@
|
|||
<?php
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Controller\AppController;
|
||||
use App\Model\Entity\Individual;
|
||||
use Cake\Utility\Inflector;
|
||||
use Cake\Utility\Hash;
|
||||
use Cake\Utility\Text;
|
||||
use \Cake\Database\Expression\QueryExpression;
|
||||
use Cake\Http\Exception\UnauthorizedException;
|
||||
use Cake\Http\Exception\NotFoundException;
|
||||
use Cake\ORM\Query;
|
||||
use Cake\ORM\Entity;
|
||||
use Exception;
|
||||
|
||||
class MailingListsController extends AppController
|
||||
{
|
||||
public $filterFields = ['MailingLists.uuid', 'MailingLists.name', 'description', 'releasability'];
|
||||
public $quickFilterFields = ['MailingLists.uuid', ['MailingLists.name' => true], ['description' => true], ['releasability' => true]];
|
||||
public $containFields = ['Users', 'Individuals', 'MetaFields'];
|
||||
public $statisticsFields = ['active'];
|
||||
|
||||
public function index()
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$this->CRUD->index([
|
||||
'contain' => $this->containFields,
|
||||
'filters' => $this->filterFields,
|
||||
'quickFilters' => $this->quickFilterFields,
|
||||
'statisticsFields' => $this->statisticsFields,
|
||||
'afterFind' => function ($row) use ($currentUser) {
|
||||
if (empty($currentUser['role']['perm_admin']) || $row['user_id'] != $currentUser['id']) {
|
||||
if (!$this->MailingLists->isIndividualListed($currentUser['individual_id'], $row)) {
|
||||
$row = false;
|
||||
}
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
|
||||
public function add()
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$this->CRUD->add([
|
||||
'override' => [
|
||||
'user_id' => $currentUser['id']
|
||||
],
|
||||
'beforeSave' => function ($data) use ($currentUser) {
|
||||
return $data;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
|
||||
public function view($id)
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$this->CRUD->view($id, [
|
||||
'contain' => $this->containFields,
|
||||
'afterFind' => function($data) use ($currentUser) {
|
||||
if (empty($currentUser['role']['perm_admin']) || $data['user_id'] != $currentUser['id']) {
|
||||
if (!$this->MailingLists->isIndividualListed($currentUser['individual_id'], $data)) {
|
||||
$data = [];
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
},
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
|
||||
public function edit($id = false)
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$params = [];
|
||||
if (empty($currentUser['role']['perm_admin'])) {
|
||||
$params['conditions'] = ['user_id' => $currentUser['id']];
|
||||
}
|
||||
$this->CRUD->edit($id, $params);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->render('add');
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
if (empty($currentUser['role']['perm_admin'])) {
|
||||
$params['conditions'] = ['user_id' => $currentUser['id']];
|
||||
}
|
||||
$this->CRUD->delete($id, $params);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
|
||||
public function listIndividuals($mailinglist_id)
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$quickFilter = [
|
||||
'uuid',
|
||||
['first_name' => true],
|
||||
['last_name' => true],
|
||||
];
|
||||
$quickFilterUI = array_merge($quickFilter, [
|
||||
['Registered emails' => true],
|
||||
]);
|
||||
$filters = ['uuid', 'first_name', 'last_name', 'quickFilter'];
|
||||
$queryParams = $this->ParamHandler->harvestParams($filters);
|
||||
$activeFilters = $queryParams['quickFilter'] ?? [];
|
||||
|
||||
$mailingList = $this->MailingLists->find()
|
||||
->where(['MailingLists.id' => $mailinglist_id])
|
||||
->contain(['MetaFields', 'Individuals'])
|
||||
->first();
|
||||
|
||||
if (is_null($mailingList)) {
|
||||
throw new NotFoundException(__('Invalid {0}.', Inflector::singularize($this->MailingLists->getAlias())));
|
||||
}
|
||||
if (empty($currentUser['role']['perm_admin']) || $mailingList['user_id'] != $currentUser['id']) {
|
||||
if (!$this->MailingLists->isIndividualListed($currentUser['individual_id'], $mailingList)) {
|
||||
throw new NotFoundException(__('Invalid {0}.', Inflector::singularize($this->MailingLists->getAlias())));
|
||||
}
|
||||
}
|
||||
|
||||
$filteringActive = !empty($queryParams['quickFilter']);
|
||||
$matchingMetaFieldParentIDs = [];
|
||||
if ($filteringActive) {
|
||||
// Collect individuals having a matching meta_field for the requested search value
|
||||
foreach ($mailingList->meta_fields as $metaField) {
|
||||
if (
|
||||
empty($queryParams['quickFilter']) ||
|
||||
(
|
||||
str_contains($metaField->field, 'email') &&
|
||||
str_contains($metaField->value, $queryParams['quickFilter'])
|
||||
)
|
||||
) {
|
||||
$matchingMetaFieldParentIDs[$metaField->parent_id] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
$matchingMetaFieldParentIDs = array_keys($matchingMetaFieldParentIDs);
|
||||
unset($mailingList['individuals']); // reset loaded individuals for the filtering to take effect
|
||||
// Perform filtering based on the searched values (supports emails from metafield or individual)
|
||||
$mailingList = $this->MailingLists->loadInto($mailingList, [
|
||||
'Individuals' => function (Query $q) use ($queryParams, $quickFilter, $filteringActive, $matchingMetaFieldParentIDs) {
|
||||
$conditions = [];
|
||||
if (!empty($queryParams)) {
|
||||
$conditions = $this->CRUD->genQuickFilterConditions($queryParams, $quickFilter);
|
||||
}
|
||||
if ($filteringActive && !empty($matchingMetaFieldParentIDs)) {
|
||||
$conditions[] = function (QueryExpression $exp) use ($matchingMetaFieldParentIDs) {
|
||||
return $exp->in('Individuals.id', $matchingMetaFieldParentIDs);
|
||||
};
|
||||
}
|
||||
if ($filteringActive && !empty($queryParams['quickFilter'])) {
|
||||
$conditions[] = [
|
||||
'MailingListsIndividuals.include_primary_email' => true,
|
||||
'Individuals.email LIKE' => "%{$queryParams['quickFilter']}%"
|
||||
];
|
||||
}
|
||||
$q->where([
|
||||
'OR' => $conditions
|
||||
]);
|
||||
return $q;
|
||||
}
|
||||
]);
|
||||
$mailingList->injectRegisteredEmailsIntoIndividuals();
|
||||
if ($this->ParamHandler->isRest()) {
|
||||
return $this->RestResponse->viewData($mailingList->individuals, 'json');
|
||||
}
|
||||
$individuals = $this->CustomPagination->paginate($mailingList->individuals);
|
||||
$this->set('mailing_list_id', $mailinglist_id);
|
||||
$this->set('quickFilter', $quickFilterUI);
|
||||
$this->set('activeFilters', $activeFilters);
|
||||
$this->set('quickFilterValue', $queryParams['quickFilter'] ?? '');
|
||||
$this->set('individuals', $individuals);
|
||||
}
|
||||
|
||||
public function addIndividual($mailinglist_id)
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$params = [
|
||||
'contain' => ['Individuals', 'MetaFields']
|
||||
];
|
||||
if (empty($currentUser['role']['perm_admin'])) {
|
||||
$params['conditions'] = ['user_id' => $currentUser['id']];
|
||||
}
|
||||
$mailingList = $this->MailingLists->get($mailinglist_id, $params);
|
||||
$linkedIndividualsIDs = Hash::extract($mailingList, 'individuals.{n}.id');
|
||||
$conditions = [];
|
||||
if (!empty($linkedIndividualsIDs)) {
|
||||
$conditions = [
|
||||
'id NOT IN' => $linkedIndividualsIDs
|
||||
];
|
||||
}
|
||||
$dropdownData = [
|
||||
'individuals' => $this->MailingLists->Individuals->getTarget()->find()
|
||||
->order(['first_name' => 'asc'])
|
||||
->where($conditions)
|
||||
->all()
|
||||
->combine('id', 'full_name')
|
||||
->toArray()
|
||||
];
|
||||
if ($this->request->is('post') || $this->request->is('put')) {
|
||||
$memberIDs = $this->request->getData()['individuals'];
|
||||
$chosen_emails = $this->request->getData()['chosen_emails'];
|
||||
if (!empty($chosen_emails)) {
|
||||
$chosen_emails = json_decode($chosen_emails, true);
|
||||
$chosen_emails = !is_null($chosen_emails) ? $chosen_emails : [];
|
||||
} else {
|
||||
$chosen_emails = [];
|
||||
}
|
||||
$members = $this->MailingLists->Individuals->getTarget()->find()->where([
|
||||
'id IN' => $memberIDs
|
||||
])->all()->toArray();
|
||||
$memberToLink = [];
|
||||
foreach ($members as $i => $member) {
|
||||
$includePrimary = in_array('primary', $chosen_emails[$member->id]);
|
||||
$chosen_emails[$member->id] = array_filter($chosen_emails[$member->id], function($entry) {
|
||||
return $entry != 'primary';
|
||||
});
|
||||
$members[$i]->_joinData = new Entity(['include_primary_email' => $includePrimary]);
|
||||
if (!in_array($member->id, $linkedIndividualsIDs)) { // individual are not already in the list
|
||||
$memberToLink[] = $members[$i];
|
||||
}
|
||||
}
|
||||
|
||||
// save new individuals
|
||||
if (!empty($memberToLink)) {
|
||||
$success = (bool)$this->MailingLists->Individuals->link($mailingList, $memberToLink);
|
||||
if ($success && !empty($chosen_emails[$member->id])) { // Include any remaining emails from the metaFields
|
||||
$emailsFromMetaFields = $this->MailingLists->MetaFields->find()->where([
|
||||
'id IN' => $chosen_emails[$member->id]
|
||||
])->all()->toArray();
|
||||
$success = (bool)$this->MailingLists->MetaFields->link($mailingList, $emailsFromMetaFields);
|
||||
}
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
$message = __n('{0} individual added to the mailing list.', '{0} Individuals added to the mailing list.', count($members), count($members));
|
||||
$mailingList = $this->MailingLists->get($mailingList->id);
|
||||
} else {
|
||||
$message = __n('The individual could not be added to the mailing list.', 'The Individuals could not be added to the mailing list.', count($members));
|
||||
}
|
||||
$this->CRUD->setResponseForController('add_individuals', $success, $message, $mailingList, $mailingList->getErrors());
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
$this->set(compact('dropdownData'));
|
||||
$this->set('mailinglist_id', $mailinglist_id);
|
||||
$this->set('mailingList', $mailingList);
|
||||
}
|
||||
|
||||
public function removeIndividual($mailinglist_id, $individual_id=null)
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$params = [
|
||||
'contain' => ['Individuals', 'MetaFields']
|
||||
];
|
||||
if (empty($currentUser['role']['perm_admin'])) {
|
||||
$params['conditions'] = ['user_id' => $currentUser['id']];
|
||||
}
|
||||
$mailingList = $this->MailingLists->get($mailinglist_id, $params);
|
||||
$individual = [];
|
||||
if (!is_null($individual_id)) {
|
||||
$individual = $this->MailingLists->Individuals->get($individual_id);
|
||||
}
|
||||
if ($this->request->is('post') || $this->request->is('delete')) {
|
||||
$success = false;
|
||||
if (!is_null($individual_id)) {
|
||||
$individualToRemove = $this->MailingLists->Individuals->get($individual_id);
|
||||
$metaFieldsIDsToRemove = Hash::extract($mailingList, 'meta_fields.{n}.id');
|
||||
if (!empty($metaFieldsIDsToRemove)) {
|
||||
$metaFieldsToRemove = $this->MailingLists->MetaFields->find()->where([
|
||||
'id IN' => $metaFieldsIDsToRemove,
|
||||
'parent_id' => $individual_id,
|
||||
])->all()->toArray();
|
||||
}
|
||||
$success = (bool)$this->MailingLists->Individuals->unlink($mailingList, [$individualToRemove]);
|
||||
if ($success && !empty($metaFieldsToRemove)) {
|
||||
$success = (bool)$this->MailingLists->MetaFields->unlink($mailingList, $metaFieldsToRemove);
|
||||
}
|
||||
if ($success) {
|
||||
$message = __('{0} removed from the mailing list.', $individualToRemove->full_name);
|
||||
$mailingList = $this->MailingLists->get($mailingList->id);
|
||||
} else {
|
||||
$message = __n('{0} could not be removed from the mailing list.', $individual->full_name);
|
||||
}
|
||||
$this->CRUD->setResponseForController('remove_individuals', $success, $message, $mailingList, $mailingList->getErrors());
|
||||
} else {
|
||||
$params = $this->ParamHandler->harvestParams(['ids']);
|
||||
if (!empty($params['ids'])) {
|
||||
$params['ids'] = json_decode($params['ids']);
|
||||
}
|
||||
if (empty($params['ids'])) {
|
||||
throw new NotFoundException(__('Invalid {0}.', Inflector::singularize($this->MailingLists->Individuals->getAlias())));
|
||||
}
|
||||
$individualsToRemove = $this->MailingLists->Individuals->find()->where([
|
||||
'id IN' => array_map('intval', $params['ids'])
|
||||
])->all()->toArray();
|
||||
$metaFieldsIDsToRemove = Hash::extract($mailingList, 'meta_fields.{n}.id');
|
||||
if (!empty($metaFieldsIDsToRemove)) {
|
||||
$metaFieldsToRemove = $this->MailingLists->MetaFields->find()->where([
|
||||
'id IN' => $metaFieldsIDsToRemove,
|
||||
])->all()->toArray();
|
||||
}
|
||||
$unlinkSuccesses = 0;
|
||||
foreach ($individualsToRemove as $individualToRemove) {
|
||||
$success = (bool)$this->MailingLists->Individuals->unlink($mailingList, [$individualToRemove]);
|
||||
$results[] = $success;
|
||||
if ($success) {
|
||||
$unlinkSuccesses++;
|
||||
}
|
||||
}
|
||||
$mailingList = $this->MailingLists->get($mailingList->id);
|
||||
$success = $unlinkSuccesses == count($individualsToRemove);
|
||||
$message = __(
|
||||
'{0} {1} have been removed.',
|
||||
$unlinkSuccesses == count($individualsToRemove) ? __('All') : sprintf('%s / %s', $unlinkSuccesses, count($individualsToRemove)),
|
||||
Inflector::singularize($this->MailingLists->Individuals->getAlias())
|
||||
);
|
||||
$this->CRUD->setResponseForController('remove_individuals', $success, $message, $mailingList, []);
|
||||
}
|
||||
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
$this->set('mailinglist_id', $mailinglist_id);
|
||||
$this->set('mailingList', $mailingList);
|
||||
if (!empty($individual)) {
|
||||
$this->set('deletionText', __('Are you sure you want to remove `{0} ({1})` from the mailing list?', $individual->full_name, $individual->email));
|
||||
} else {
|
||||
$this->set('deletionText', __('Are you sure you want to remove multiples individuals from the mailing list?'));
|
||||
}
|
||||
$this->set('postLinkParameters', ['action' => 'removeIndividual', $mailinglist_id, $individual_id]);
|
||||
$this->viewBuilder()->setLayout('ajax');
|
||||
$this->render('/genericTemplates/delete');
|
||||
}
|
||||
}
|
|
@ -5,62 +5,285 @@ namespace App\Controller;
|
|||
use App\Controller\AppController;
|
||||
use Cake\Utility\Hash;
|
||||
use Cake\Utility\Text;
|
||||
use Cake\Utility\Inflector;
|
||||
use Cake\ORM\TableRegistry;
|
||||
use \Cake\Database\Expression\QueryExpression;
|
||||
use Cake\Http\Exception\NotFoundException;
|
||||
use Cake\Http\Exception\MethodNotAllowedException;
|
||||
use Cake\Routing\Router;
|
||||
|
||||
|
||||
class MetaTemplatesController extends AppController
|
||||
{
|
||||
public $quickFilterFields = ['name', 'uuid', 'scope'];
|
||||
public $quickFilterFields = [['name' => true], 'uuid', ['scope' => true]];
|
||||
public $filterFields = ['name', 'uuid', 'scope', 'namespace'];
|
||||
public $containFields = ['MetaTemplateFields'];
|
||||
|
||||
public function update()
|
||||
public function updateAllTemplates()
|
||||
{
|
||||
if ($this->request->is('post')) {
|
||||
$result = $this->MetaTemplates->update();
|
||||
$result = $this->MetaTemplates->updateAllTemplates();
|
||||
if ($this->ParamHandler->isRest()) {
|
||||
return $this->RestResponse->viewData($result, 'json');
|
||||
} else {
|
||||
$this->Flash->success(__('{0} templates updated.', count($result)));
|
||||
$this->redirect($this->referer());
|
||||
if ($result['success']) {
|
||||
$message = __n('{0} templates updated.', 'The template has been updated.', empty($template_id), $result['files_processed']);
|
||||
} else {
|
||||
$message = __n('{0} templates could not be updated.', 'The template could not be updated.', empty($template_id), $result['files_processed']);
|
||||
}
|
||||
$this->CRUD->setResponseForController('updateAllTemplate', $result['success'], $message, $result['files_processed'], $result['update_errors'], ['redirect' => $this->referer()]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!$this->ParamHandler->isRest()) {
|
||||
$this->set('title', __('Update Meta Templates'));
|
||||
$this->set('question', __('Are you sure you wish to update the Meta Template definitions?'));
|
||||
$this->set('actionName', __('Update'));
|
||||
$this->set('path', ['controller' => 'metaTemplates', 'action' => 'update']);
|
||||
$this->set('title', __('Update All Meta Templates'));
|
||||
$this->set('question', __('Are you sure you wish to update all the Meta Template definitions'));
|
||||
$templatesUpdateStatus = $this->MetaTemplates->getUpdateStatusForTemplates();
|
||||
$this->set('templatesUpdateStatus', $templatesUpdateStatus);
|
||||
$this->render('updateAll');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the provided template or all templates
|
||||
*
|
||||
* @param int|null $template_id
|
||||
*/
|
||||
public function update($template_id=null)
|
||||
{
|
||||
$metaTemplate = false;
|
||||
if (!is_null($template_id)) {
|
||||
if (!is_numeric($template_id)) {
|
||||
throw new NotFoundException(__('Invalid {0} for provided ID.', $this->MetaTemplates->getAlias(), $template_id));
|
||||
}
|
||||
$metaTemplate = $this->MetaTemplates->find()->where([
|
||||
'id' => $template_id
|
||||
])->contain(['MetaTemplateFields'])->first();
|
||||
|
||||
if (empty($metaTemplate)) {
|
||||
throw new NotFoundException(__('Invalid {0} {1}.', $this->MetaTemplates->getAlias(), $template_id));
|
||||
}
|
||||
}
|
||||
if ($this->request->is('post')) {
|
||||
$params = $this->ParamHandler->harvestParams(['update_strategy']);
|
||||
$updateStrategy = $params['update_strategy'] ?? null;
|
||||
$result = $this->MetaTemplates->update($metaTemplate, $updateStrategy);
|
||||
if ($this->ParamHandler->isRest()) {
|
||||
return $this->RestResponse->viewData($result, 'json');
|
||||
} else {
|
||||
if ($result['success']) {
|
||||
$message = __n('{0} templates updated.', 'The template has been updated.', empty($template_id), $result['files_processed']);
|
||||
} else {
|
||||
$message = __n('{0} templates could not be updated.', 'The template could not be updated.', empty($template_id), $result['files_processed']);
|
||||
}
|
||||
$this->CRUD->setResponseForController('update', $result['success'], $message, $result['files_processed'], $result['update_errors'], ['redirect' => $this->referer()]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!$this->ParamHandler->isRest()) {
|
||||
if (!empty($metaTemplate)) {
|
||||
$this->set('metaTemplate', $metaTemplate);
|
||||
$statuses = $this->setUpdateStatus($metaTemplate->id);
|
||||
$this->set('updateStatus', $this->MetaTemplates->computeFullUpdateStatusForMetaTemplate($statuses['templateStatus'], $metaTemplate));
|
||||
} else {
|
||||
$this->set('title', __('Update All Meta Templates'));
|
||||
$this->set('question', __('Are you sure you wish to update all the Meta Template definitions'));
|
||||
$templatesUpdateStatus = $this->MetaTemplates->getUpdateStatusForTemplates();
|
||||
$this->set('templatesUpdateStatus', $templatesUpdateStatus);
|
||||
$this->render('updateAll');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new template by loading the template on the disk having the provided UUID.
|
||||
*
|
||||
* @param string $uuid
|
||||
*/
|
||||
public function createNewTemplate(string $uuid)
|
||||
{
|
||||
if ($this->request->is('post')) {
|
||||
$result = $this->MetaTemplates->createNewTemplate($uuid);
|
||||
if ($this->ParamHandler->isRest()) {
|
||||
return $this->RestResponse->viewData($result, 'json');
|
||||
} else {
|
||||
if ($result['success']) {
|
||||
$message = __('The template {0} has been created.', $uuid);
|
||||
} else {
|
||||
$message = __('The template {0} could not be created.', $uuid);
|
||||
}
|
||||
$this->CRUD->setResponseForController('createNewTemplate', $result['success'], $message, $result['files_processed'], $result['update_errors']);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!$this->ParamHandler->isRest()) {
|
||||
$this->set('title', __('Create Meta Template'));
|
||||
$this->set('question', __('Are you sure you wish to load the meta template with UUID: {0} in the database', h($uuid)));
|
||||
$this->set('actionName', __('Create template'));
|
||||
$this->set('path', ['controller' => 'meta-templates', 'action' => 'create_new_template', $uuid]);
|
||||
$this->render('/genericTemplates/confirm');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getMetaFieldsToUpdate($template_id)
|
||||
{
|
||||
$metaTemplate = $this->MetaTemplates->get($template_id);
|
||||
$newestMetaTemplate = $this->MetaTemplates->getNewestVersion($metaTemplate);
|
||||
$amountOfEntitiesToUpdate = 0;
|
||||
$entities = $this->MetaTemplates->getEntitiesHavingMetaFieldsFromTemplate($template_id, 10, $amountOfEntitiesToUpdate);
|
||||
$this->set('metaTemplate', $metaTemplate);
|
||||
$this->set('newestMetaTemplate', $newestMetaTemplate);
|
||||
$this->set('entities', $entities);
|
||||
$this->set('amountOfEntitiesToUpdate', $amountOfEntitiesToUpdate);
|
||||
}
|
||||
|
||||
public function migrateOldMetaTemplateToNewestVersionForEntity($template_id, $entity_id)
|
||||
{
|
||||
$metaTemplate = $this->MetaTemplates->get($template_id, [
|
||||
'contain' => ['MetaTemplateFields']
|
||||
]);
|
||||
$newestMetaTemplate = $this->MetaTemplates->getNewestVersion($metaTemplate, true);
|
||||
$entity = $this->MetaTemplates->getEntity($metaTemplate, $entity_id);
|
||||
$conditions = [
|
||||
'MetaFields.meta_template_id IN' => [$metaTemplate->id, $newestMetaTemplate->id],
|
||||
'MetaFields.scope' => $metaTemplate->scope,
|
||||
];
|
||||
$keyedMetaFields = $this->MetaTemplates->getKeyedMetaFieldsForEntity($entity_id, $conditions);
|
||||
if (empty($keyedMetaFields[$metaTemplate->id])) {
|
||||
throw new NotFoundException(__('Invalid {0}. This entities does not have meta-fields to be moved to a newer template.', $this->MetaTemplates->getAlias()));
|
||||
}
|
||||
$mergedMetaFields = $this->MetaTemplates->insertMetaFieldsInMetaTemplates($keyedMetaFields, [$metaTemplate, $newestMetaTemplate]);
|
||||
$entity['MetaTemplates'] = $mergedMetaFields;
|
||||
if ($this->request->is('post') || $this->request->is('put')) {
|
||||
$className = Inflector::camelize(Inflector::pluralize($newestMetaTemplate->scope));
|
||||
$entityTable = TableRegistry::getTableLocator()->get($className);
|
||||
$inputData = $this->request->getData();
|
||||
$massagedData = $this->MetaTemplates->massageMetaFieldsBeforeSave($entity, $inputData, $newestMetaTemplate);
|
||||
unset($inputData['MetaTemplates']); // Avoid MetaTemplates to be overriden when patching entity
|
||||
$data = $massagedData['entity'];
|
||||
$metaFieldsToDelete = $massagedData['metafields_to_delete'];
|
||||
foreach ($entity->meta_fields as $i => $metaField) {
|
||||
if ($metaField->meta_template_id == $template_id) {
|
||||
$metaFieldsToDelete[] = $entity->meta_fields[$i];
|
||||
}
|
||||
}
|
||||
$data = $entityTable->patchEntity($data, $inputData);
|
||||
$savedData = $entityTable->save($data);
|
||||
if ($savedData !== false) {
|
||||
if (!empty($metaFieldsToDelete)) {
|
||||
$entityTable->MetaFields->unlink($savedData, $metaFieldsToDelete);
|
||||
}
|
||||
$message = __('Data on old meta-template has been migrated to newest meta-template');
|
||||
} else {
|
||||
$message = __('Could not migrate data to newest meta-template');
|
||||
}
|
||||
$this->CRUD->setResponseForController(
|
||||
'migrateOldMetaTemplateToNewestVersionForEntity',
|
||||
$savedData !== false,
|
||||
$message,
|
||||
$savedData,
|
||||
[],
|
||||
[
|
||||
'redirect' => [
|
||||
'controller' => $className,
|
||||
'action' => 'view', $entity_id,
|
||||
'url' => Router::url(['controller' => $className, 'action' => 'view', $entity_id])
|
||||
]
|
||||
]
|
||||
);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
$conflicts = $this->MetaTemplates->getMetaFieldsConflictsUnderTemplate($entity->meta_fields, $newestMetaTemplate);
|
||||
foreach ($conflicts as $conflict) {
|
||||
if (!empty($conflict['existing_meta_template_field'])) {
|
||||
$existingMetaTemplateField = $conflict['existing_meta_template_field'];
|
||||
foreach ($existingMetaTemplateField->metaFields as $metaField) {
|
||||
$metaField->setError('value', implode(', ', $existingMetaTemplateField->conflicts));
|
||||
}
|
||||
}
|
||||
}
|
||||
// automatically convert non-conflicting fields to new meta-template
|
||||
$movedMetaTemplateFields = [];
|
||||
foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
|
||||
if (!empty($conflicts[$metaTemplateField->field]['conflicts'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($newestMetaTemplate->meta_template_fields as $i => $newMetaTemplateField) {
|
||||
if ($metaTemplateField->field == $newMetaTemplateField->field && empty($newMetaTemplateField->metaFields)) {
|
||||
$movedMetaTemplateFields[] = $metaTemplateField->id;
|
||||
$copiedMetaFields = array_map(function ($e) use ($newMetaTemplateField) {
|
||||
$e = $e->toArray();
|
||||
$e['meta_template_id'] = $newMetaTemplateField->meta_template_id;
|
||||
$e['meta_template_field_id'] = $newMetaTemplateField->id;
|
||||
unset($e['id']);
|
||||
return $e;
|
||||
}, $metaTemplateField->metaFields);
|
||||
$newMetaTemplateField->metaFields = $this->MetaTemplates->MetaTemplateFields->MetaFields->newEntities($copiedMetaFields);
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->set('oldMetaTemplate', $metaTemplate);
|
||||
$this->set('newMetaTemplate', $newestMetaTemplate);
|
||||
$this->set('entity', $entity);
|
||||
$this->set('conflicts', $conflicts);
|
||||
$this->set('movedMetaTemplateFields', $movedMetaTemplateFields);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$templatesUpdateStatus = $this->MetaTemplates->getUpdateStatusForTemplates();
|
||||
$this->CRUD->index([
|
||||
'filters' => $this->filterFields,
|
||||
'quickFilters' => $this->quickFilterFields,
|
||||
'contextFilters' => [
|
||||
'fields' => ['scope'],
|
||||
'custom' => [
|
||||
[
|
||||
'label' => __('Contact DB'),
|
||||
'filterCondition' => ['scope' => ['individual', 'organisation']]
|
||||
],
|
||||
[
|
||||
'label' => __('Namespace CNW'),
|
||||
'filterCondition' => ['namespace' => 'cnw']
|
||||
'default' => true,
|
||||
'label' => __('Latest Templates'),
|
||||
'filterConditionFunction' => function ($query) {
|
||||
return $query->where([
|
||||
'id IN' => $this->MetaTemplates->genQueryForAllNewestVersionIDs()
|
||||
]);
|
||||
}
|
||||
],
|
||||
]
|
||||
],
|
||||
'contain' => $this->containFields
|
||||
'contain' => $this->containFields,
|
||||
'afterFind' => function($metaTemplate) use ($templatesUpdateStatus) {
|
||||
if (!empty($templatesUpdateStatus[$metaTemplate->uuid])) {
|
||||
$templateStatus = $this->MetaTemplates->getStatusForMetaTemplate($templatesUpdateStatus[$metaTemplate->uuid]['template'], $metaTemplate);
|
||||
$metaTemplate->set('updateStatus', $this->MetaTemplates->computeFullUpdateStatusForMetaTemplate($templateStatus, $metaTemplate));
|
||||
}
|
||||
return $metaTemplate;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$updateableTemplates = [
|
||||
'not-up-to-date' => $this->MetaTemplates->getNotUpToDateTemplates(),
|
||||
'can-be-removed' => $this->MetaTemplates->getCanBeRemovedTemplates(),
|
||||
'new' => $this->MetaTemplates->getNewTemplates(),
|
||||
];
|
||||
$this->set('defaultTemplatePerScope', $this->MetaTemplates->getDefaultTemplatePerScope());
|
||||
$this->set('alignmentScope', 'individuals');
|
||||
$this->set('metaGroup', 'Administration');
|
||||
$this->set('updateableTemplates', $updateableTemplates);
|
||||
}
|
||||
|
||||
public function view($id)
|
||||
|
@ -72,7 +295,25 @@ class MetaTemplatesController extends AppController
|
|||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'Administration');
|
||||
$this->setUpdateStatus($id);
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$metaTemplate = $this->MetaTemplates->get($id, [
|
||||
'contain' => ['MetaTemplateFields']
|
||||
]);
|
||||
$templateOnDisk = $this->MetaTemplates->readTemplateFromDisk($metaTemplate->uuid);
|
||||
$templateStatus = $this->MetaTemplates->getStatusForMetaTemplate($templateOnDisk, $metaTemplate);
|
||||
if (empty($templateStatus['can-be-removed'])) {
|
||||
throw new MethodNotAllowedException(__('This meta-template cannot be removed'));
|
||||
}
|
||||
$this->set('deletionText', __('The meta-template "{0}" has no meta-field and can be safely removed.', h($templateStatus['existing_template']->name)));
|
||||
$this->CRUD->delete($id);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
|
||||
public function toggle($id, $fieldName = 'enabled')
|
||||
|
@ -89,4 +330,35 @@ class MetaTemplatesController extends AppController
|
|||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
|
||||
private function getUpdateStatus($id): array
|
||||
{
|
||||
$metaTemplate = $this->MetaTemplates->get($id, [
|
||||
'contain' => ['MetaTemplateFields']
|
||||
]);
|
||||
$templateOnDisk = $this->MetaTemplates->readTemplateFromDisk($metaTemplate->uuid);
|
||||
$templateStatus = $this->MetaTemplates->getStatusForMetaTemplate($templateOnDisk, $metaTemplate);
|
||||
return $templateStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retreive the template stored on disk and compute the status for the provided template id.
|
||||
*
|
||||
* @param [type] $id
|
||||
* @return array
|
||||
*/
|
||||
private function setUpdateStatus($template_id): array
|
||||
{
|
||||
$metaTemplate = $this->MetaTemplates->get($template_id, [
|
||||
'contain' => ['MetaTemplateFields']
|
||||
]);
|
||||
$templateOnDisk = $this->MetaTemplates->readTemplateFromDisk($metaTemplate->uuid);
|
||||
$templateStatus = $this->MetaTemplates->getStatusForMetaTemplate($templateOnDisk, $metaTemplate);
|
||||
$this->set('templateOnDisk', $templateOnDisk);
|
||||
$this->set('templateStatus', $templateStatus);
|
||||
return [
|
||||
'templateOnDisk' => $templateOnDisk,
|
||||
'templateStatus' => $templateStatus,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,12 +16,14 @@ class OrganisationsController extends AppController
|
|||
public $quickFilterFields = [['name' => true], 'uuid', 'nationality', 'sector', 'type', 'url'];
|
||||
public $filterFields = ['name', 'uuid', 'nationality', 'sector', 'type', 'url', 'Alignments.id', 'MetaFields.field', 'MetaFields.value', 'MetaFields.MetaTemplates.name'];
|
||||
public $containFields = ['Alignments' => 'Individuals'];
|
||||
public $statisticsFields = ['nationality', 'sector'];
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->CRUD->index([
|
||||
'filters' => $this->filterFields,
|
||||
'quickFilters' => $this->quickFilterFields,
|
||||
'quickFilterForMetaField' => ['enabled' => true, 'wildcard_search' => true],
|
||||
'contextFilters' => [
|
||||
'custom' => [
|
||||
[
|
||||
|
@ -59,7 +61,8 @@ class OrganisationsController extends AppController
|
|||
]
|
||||
],
|
||||
],
|
||||
'contain' => $this->containFields
|
||||
'contain' => $this->containFields,
|
||||
'statisticsFields' => $this->statisticsFields,
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
|
|
|
@ -19,20 +19,32 @@ class SharingGroupsController extends AppController
|
|||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$conditions = [];
|
||||
if (empty($currentUser['role']['perm_admin'])) {
|
||||
$conditions['SharingGroups.organisation_id'] = $currentUser['organisation_id'];
|
||||
}
|
||||
$this->CRUD->index([
|
||||
'contain' => $this->containFields,
|
||||
'filters' => $this->filterFields,
|
||||
'quickFilters' => $this->quickFilterFields,
|
||||
'conditions' => $conditions
|
||||
'conditions' => $conditions,
|
||||
'afterFind' => function ($row) use ($currentUser) {
|
||||
if (empty($currentUser['role']['perm_admin'])) {
|
||||
$orgFound = false;
|
||||
if (!empty($row['sharing_group_orgs'])) {
|
||||
foreach ($row['sharing_group_orgs'] as $org) {
|
||||
if ($org['id'] === $currentUser['organisation_id']) {
|
||||
$orgFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($row['organisation_id'] !== $currentUser['organisation_id'] && !$orgFound) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'Trust Circles');
|
||||
}
|
||||
|
||||
public function add()
|
||||
|
@ -57,7 +69,6 @@ class SharingGroupsController extends AppController
|
|||
return $responsePayload;
|
||||
}
|
||||
$this->set(compact('dropdownData'));
|
||||
$this->set('metaGroup', 'Trust Circles');
|
||||
}
|
||||
|
||||
public function view($id)
|
||||
|
@ -67,7 +78,7 @@ class SharingGroupsController extends AppController
|
|||
'contain' => ['SharingGroupOrgs', 'Organisations', 'Users' => ['fields' => ['id', 'username']]],
|
||||
'afterFind' => function($data) use ($currentUser) {
|
||||
if (empty($currentUser['role']['perm_admin'])) {
|
||||
$orgFround = false;
|
||||
$orgFound = false;
|
||||
if (!empty($data['sharing_group_orgs'])) {
|
||||
foreach ($data['sharing_group_orgs'] as $org) {
|
||||
if ($org['id'] === $currentUser['organisation_id']) {
|
||||
|
@ -75,7 +86,7 @@ class SharingGroupsController extends AppController
|
|||
}
|
||||
}
|
||||
}
|
||||
if ($data['organisation_id'] !== $currentUser['organisation_id'] && !$orgFround) {
|
||||
if ($data['organisation_id'] !== $currentUser['organisation_id'] && !$orgFound) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +97,6 @@ class SharingGroupsController extends AppController
|
|||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'Trust Circles');
|
||||
}
|
||||
|
||||
public function edit($id = false)
|
||||
|
@ -106,13 +116,13 @@ class SharingGroupsController extends AppController
|
|||
'organisation' => $this->getAvailableOrgForSg($this->ACL->getUser())
|
||||
];
|
||||
$this->set(compact('dropdownData'));
|
||||
$this->set('metaGroup', 'Trust Circles');
|
||||
$this->render('add');
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$params = [];
|
||||
if (empty($currentUser['role']['perm_admin'])) {
|
||||
$params['conditions'] = ['organisation_id' => $currentUser['organisation_id']];
|
||||
}
|
||||
|
@ -121,7 +131,6 @@ class SharingGroupsController extends AppController
|
|||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'Trust Circles');
|
||||
}
|
||||
|
||||
public function addOrg($id)
|
||||
|
|
|
@ -2,10 +2,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;
|
||||
use Cake\Http\Exception\UnauthorizedException;
|
||||
use Cake\Http\Exception\MethodNotAllowedException;
|
||||
use Cake\Core\Configure;
|
||||
|
@ -163,11 +160,6 @@ class UsersController extends AppController
|
|||
}
|
||||
|
||||
$params = [
|
||||
'get' => [
|
||||
'fields' => [
|
||||
'id', 'individual_id', 'role_id', 'disabled', 'username'
|
||||
]
|
||||
],
|
||||
'removeEmpty' => [
|
||||
'password'
|
||||
],
|
||||
|
@ -175,12 +167,15 @@ class UsersController extends AppController
|
|||
'password', 'confirm_password'
|
||||
]
|
||||
];
|
||||
if (!empty($this->ACL->getUser()['role']['perm_admin'])) {
|
||||
if ($this->request->is(['get'])) {
|
||||
$params['fields'] = array_merge($params['fields'], ['individual_id', 'role_id', 'disabled', 'username']);
|
||||
}
|
||||
if ($this->request->is(['post', 'put']) && !empty($this->ACL->getUser()['role']['perm_admin'])) {
|
||||
$params['fields'][] = 'individual_id';
|
||||
$params['fields'][] = 'role_id';
|
||||
$params['fields'][] = 'organisation_id';
|
||||
$params['fields'][] = 'disabled';
|
||||
} else if (!empty($this->ACL->getUser()['role']['perm_org_admin'])) {
|
||||
} else if ($this->request->is(['post', 'put']) && !empty($this->ACL->getUser()['role']['perm_org_admin'])) {
|
||||
$params['fields'][] = 'role_id';
|
||||
$params['fields'][] = 'disabled';
|
||||
if (!$currentUser['role']['perm_admin']) {
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
<?php
|
||||
|
||||
namespace App\Lib\Tools;
|
||||
|
||||
class CidrTool
|
||||
{
|
||||
/** @var array */
|
||||
private $ipv4 = [];
|
||||
|
||||
/**
|
||||
* Minimum netmask for IPv4 in list. 33 because maximum netmask is 32..
|
||||
* @var int
|
||||
*/
|
||||
private $minimumIpv4Mask = 33;
|
||||
|
||||
/** @var array */
|
||||
private $ipv6 = [];
|
||||
|
||||
public function __construct(array $list)
|
||||
{
|
||||
$this->filterInputList($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value IPv4 or IPv6 address or range
|
||||
* @return false|string
|
||||
*/
|
||||
public function contains($value)
|
||||
{
|
||||
$valueMask = null;
|
||||
if (strpos($value, '/') !== false) {
|
||||
list($value, $valueMask) = explode('/', $value);
|
||||
}
|
||||
|
||||
$match = false;
|
||||
if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
// This code converts IP address to all possible CIDRs that can contains given IP address
|
||||
// and then check if given hash table contains that CIDR.
|
||||
$ip = ip2long($value);
|
||||
// Start from 1, because doesn't make sense to check 0.0.0.0/0 match
|
||||
for ($bits = $this->minimumIpv4Mask; $bits <= 32; $bits++) {
|
||||
$mask = -1 << (32 - $bits);
|
||||
$needle = long2ip($ip & $mask) . "/$bits";
|
||||
if (isset($this->ipv4[$needle])) {
|
||||
$match = $needle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} elseif (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$value = unpack('n*', inet_pton($value));
|
||||
foreach ($this->ipv6 as $netmask => $lv) {
|
||||
foreach ($lv as $l) {
|
||||
if ($this->ipv6InCidr($value, $l, $netmask)) {
|
||||
$match = inet_ntop($l) . "/$netmask";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($match && $valueMask) {
|
||||
$matchMask = explode('/', $match)[1];
|
||||
if ($valueMask < $matchMask) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $match;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $cidr
|
||||
* @return bool
|
||||
*/
|
||||
public static function validate($cidr)
|
||||
{
|
||||
$parts = explode('/', $cidr, 2);
|
||||
$ipBytes = inet_pton($parts[0]);
|
||||
if ($ipBytes === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$maximumNetmask = strlen($ipBytes) === 4 ? 32 : 128;
|
||||
if (isset($parts[1]) && ($parts[1] > $maximumNetmask || $parts[1] < 0)) {
|
||||
return false; // Netmask part of CIDR is invalid
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Using solution from https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpFoundation/IpUtils.php
|
||||
*
|
||||
* @param array $ip
|
||||
* @param string $cidr
|
||||
* @param int $netmask
|
||||
* @return bool
|
||||
*/
|
||||
private function ipv6InCidr($ip, $cidr, $netmask)
|
||||
{
|
||||
$bytesAddr = unpack('n*', $cidr);
|
||||
for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) {
|
||||
$left = $netmask - 16 * ($i - 1);
|
||||
$left = ($left <= 16) ? $left : 16;
|
||||
$mask = ~(0xffff >> $left) & 0xffff;
|
||||
if (($bytesAddr[$i] & $mask) != ($ip[$i] & $mask)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out invalid IPv4 or IPv4 CIDR and append maximum netmask if no netmask is given.
|
||||
* @param array $list
|
||||
*/
|
||||
private function filterInputList(array $list)
|
||||
{
|
||||
foreach ($list as $v) {
|
||||
$parts = explode('/', $v, 2);
|
||||
$ipBytes = inet_pton($parts[0]);
|
||||
if ($ipBytes === false) {
|
||||
continue; // IP address part of CIDR is invalid
|
||||
}
|
||||
$maximumNetmask = strlen($ipBytes) === 4 ? 32 : 128;
|
||||
|
||||
if (isset($parts[1]) && ($parts[1] > $maximumNetmask || $parts[1] < 0)) {
|
||||
// Netmask part of CIDR is invalid
|
||||
continue;
|
||||
}
|
||||
|
||||
$mask = isset($parts[1]) ? $parts[1] : $maximumNetmask;
|
||||
if ($maximumNetmask === 32) {
|
||||
if ($mask < $this->minimumIpv4Mask) {
|
||||
$this->minimumIpv4Mask = (int)$mask;
|
||||
}
|
||||
if (!isset($parts[1])) {
|
||||
$v = "$v/$maximumNetmask"; // If CIDR doesnt contains '/', we will consider CIDR as /32
|
||||
}
|
||||
$this->ipv4[$v] = true;
|
||||
} else {
|
||||
$this->ipv6[$mask][] = $ipBytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -305,70 +305,71 @@ class MispConnector extends CommonConnectorTools
|
|||
$response = $this->getData('/servers/serverSettings', $params);
|
||||
$data = $response->getJson();
|
||||
if (!empty($data['finalSettings'])) {
|
||||
$finalSettings = [
|
||||
'type' => 'index',
|
||||
'data' => [
|
||||
'data' => $data['finalSettings'],
|
||||
'skip_pagination' => 1,
|
||||
'top_bar' => [
|
||||
'children' => [
|
||||
$finalSettings = [
|
||||
'type' => 'index',
|
||||
'data' => [
|
||||
'data' => $data['finalSettings'],
|
||||
'skip_pagination' => 1,
|
||||
'top_bar' => [
|
||||
'children' => [
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
'additionalUrlParams' => $urlParams
|
||||
]
|
||||
]
|
||||
],
|
||||
'fields' => [
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
'additionalUrlParams' => $urlParams
|
||||
'name' => 'Setting',
|
||||
'sort' => 'setting',
|
||||
'data_path' => 'setting',
|
||||
],
|
||||
[
|
||||
'name' => 'Criticality',
|
||||
'sort' => 'level',
|
||||
'data_path' => 'level',
|
||||
'arrayData' => [
|
||||
0 => 'Critical',
|
||||
1 => 'Recommended',
|
||||
2 => 'Optional'
|
||||
],
|
||||
'element' => 'array_lookup_field'
|
||||
],
|
||||
[
|
||||
'name' => __('Value'),
|
||||
'sort' => 'value',
|
||||
'data_path' => 'value',
|
||||
'options' => 'options'
|
||||
],
|
||||
[
|
||||
'name' => __('Type'),
|
||||
'sort' => 'type',
|
||||
'data_path' => 'type',
|
||||
],
|
||||
[
|
||||
'name' => __('Error message'),
|
||||
'sort' => 'errorMessage',
|
||||
'data_path' => 'errorMessage',
|
||||
]
|
||||
],
|
||||
'title' => false,
|
||||
'description' => false,
|
||||
'pull' => 'right',
|
||||
'actions' => [
|
||||
[
|
||||
'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/modifySettingAction?setting={{0}}',
|
||||
'modal_params_data_path' => ['setting'],
|
||||
'icon' => 'download',
|
||||
'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/ServerSettingsAction'
|
||||
]
|
||||
]
|
||||
],
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'Setting',
|
||||
'sort' => 'setting',
|
||||
'data_path' => 'setting',
|
||||
],
|
||||
[
|
||||
'name' => 'Criticality',
|
||||
'sort' => 'level',
|
||||
'data_path' => 'level',
|
||||
'arrayData' => [
|
||||
0 => 'Critical',
|
||||
1 => 'Recommended',
|
||||
2 => 'Optional'
|
||||
],
|
||||
'element' => 'array_lookup_field'
|
||||
],
|
||||
[
|
||||
'name' => __('Value'),
|
||||
'sort' => 'value',
|
||||
'data_path' => 'value',
|
||||
'options' => 'options'
|
||||
],
|
||||
[
|
||||
'name' => __('Type'),
|
||||
'sort' => 'type',
|
||||
'data_path' => 'type',
|
||||
],
|
||||
[
|
||||
'name' => __('Error message'),
|
||||
'sort' => 'errorMessage',
|
||||
'data_path' => 'errorMessage',
|
||||
]
|
||||
],
|
||||
'title' => false,
|
||||
'description' => false,
|
||||
'pull' => 'right',
|
||||
'actions' => [
|
||||
[
|
||||
'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/modifySettingAction?setting={{0}}',
|
||||
'modal_params_data_path' => ['setting'],
|
||||
'icon' => 'download',
|
||||
'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/ServerSettingsAction'
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
];
|
||||
|
||||
if (!empty($params['quickFilter'])) {
|
||||
$needle = strtolower($params['quickFilter']);
|
||||
foreach ($finalSettings['data']['data'] as $k => $v) {
|
||||
|
@ -404,7 +405,7 @@ class MispConnector extends CommonConnectorTools
|
|||
'children' => [
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
|
@ -494,7 +495,7 @@ class MispConnector extends CommonConnectorTools
|
|||
'children' => [
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
|
@ -572,7 +573,7 @@ class MispConnector extends CommonConnectorTools
|
|||
'children' => [
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
|
@ -640,7 +641,7 @@ class MispConnector extends CommonConnectorTools
|
|||
'children' => [
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
|
|
|
@ -103,7 +103,7 @@ class SkeletonConnector extends CommonConnectorTools
|
|||
'children' => [
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
namespace MetaFieldsTypes;
|
||||
|
||||
use Cake\Database\Expression\QueryExpression;
|
||||
use Cake\ORM\TableRegistry;
|
||||
use Cake\ORM\Query;
|
||||
|
||||
use MetaFieldsTypes\TextType;
|
||||
use TypeError;
|
||||
use App\Lib\Tools\CidrTool;
|
||||
|
||||
class IPv4Type extends TextType
|
||||
{
|
||||
public const OPERATORS = ['contains', 'excludes'];
|
||||
public const TYPE = 'ipv4';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the provided value against the expected type
|
||||
*
|
||||
* @param string $value
|
||||
* @return boolean
|
||||
*/
|
||||
public function validate(string $value): bool
|
||||
{
|
||||
return $this->_isValidIP($value) || $this->_isValidIP(explode('/', $value)[0]);
|
||||
}
|
||||
|
||||
public function setQueryExpression(QueryExpression $exp, string $searchValue, \App\Model\Entity\MetaTemplateField $metaTemplateField): QueryExpression
|
||||
{
|
||||
if (strpos($searchValue, '%') !== false) {
|
||||
$textHandler = new TextType(); // we are wildcard filtering, use text filter instead
|
||||
return $textHandler->setQueryExpression($exp, $searchValue, $metaTemplateField);
|
||||
}
|
||||
$allMetaValues = $this->fetchAllValuesForThisType([], $metaTemplateField);
|
||||
$isNegation = false;
|
||||
if (substr($searchValue, 0, 1) == '!') {
|
||||
$searchValue = substr($searchValue, 1);
|
||||
$isNegation = true;
|
||||
}
|
||||
|
||||
foreach ($allMetaValues as $fieldID => $ip) {
|
||||
$cidrTool = new CidrTool([$ip]);
|
||||
if ($cidrTool->contains($searchValue) === false) {
|
||||
if (!$isNegation) {
|
||||
unset($allMetaValues[$fieldID]);
|
||||
}
|
||||
} else if ($isNegation) {
|
||||
unset($allMetaValues[$fieldID]);
|
||||
}
|
||||
}
|
||||
$matchingIDs = array_keys($allMetaValues);
|
||||
if (!empty($matchingIDs)) {
|
||||
$exp->in('MetaFields.id', $matchingIDs);
|
||||
} else {
|
||||
$exp->eq('MetaFields.id', -1); // No matching meta-fields, generate an impossible condition to return nothing
|
||||
}
|
||||
return $exp;
|
||||
}
|
||||
|
||||
protected function fetchAllMetatemplateFieldsIdForThisType(\App\Model\Entity\MetaTemplateField $metaTemplateField = null): Query
|
||||
{
|
||||
$this->MetaTemplateFields = TableRegistry::getTableLocator()->get('MetaTemplateFields');
|
||||
$conditions = [];
|
||||
if (!is_null($metaTemplateField)) {
|
||||
$conditions['id'] = $metaTemplateField->id;
|
||||
} else {
|
||||
$conditions['type'] = $this::TYPE;
|
||||
}
|
||||
$query = $this->MetaTemplateFields->find()->select(['id'])
|
||||
->distinct()
|
||||
->where($conditions);
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function fetchAllValuesForThisType(array $conditions=[], \App\Model\Entity\MetaTemplateField $metaTemplateField=null): array
|
||||
{
|
||||
$metaTemplateFieldsIDs = $this->fetchAllMetatemplateFieldsIdForThisType($metaTemplateField);
|
||||
if (empty($metaTemplateFieldsIDs)) {
|
||||
return [];
|
||||
}
|
||||
$conditions = array_merge($conditions, ['meta_template_field_id IN' => $metaTemplateFieldsIDs]);
|
||||
$allMetaValues = $this->MetaFields->find('list', [
|
||||
'keyField' => 'id',
|
||||
'valueField' => 'value'
|
||||
])->where($conditions)->toArray();
|
||||
return $allMetaValues;
|
||||
}
|
||||
|
||||
protected function _isValidIP(string $value): bool
|
||||
{
|
||||
return filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace MetaFieldsTypes;
|
||||
|
||||
use MetaFieldsTypes\IPv4Type;
|
||||
|
||||
class IPv6Type extends IPv4Type
|
||||
{
|
||||
public const TYPE = 'ipv6';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function _isValidIP(string $value): bool
|
||||
{
|
||||
return filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
namespace MetaFieldsTypes;
|
||||
|
||||
use Cake\Database\Expression\QueryExpression;
|
||||
use Cake\ORM\TableRegistry;
|
||||
|
||||
class TextType
|
||||
{
|
||||
public const OPERATORS = ['=', '!='];
|
||||
public const TYPE = 'text';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->MetaFields = TableRegistry::getTableLocator()->get('MetaFields');
|
||||
}
|
||||
|
||||
public function validate(string $value): bool
|
||||
{
|
||||
return is_string($value);
|
||||
}
|
||||
|
||||
public function setQueryExpression(QueryExpression $exp, string $searchValue, \App\Model\Entity\MetaTemplateField $metaTemplateField): QueryExpression
|
||||
{
|
||||
$field = 'MetaFields.value';
|
||||
if (substr($searchValue, 0, 1) == '!') {
|
||||
$searchValue = substr($searchValue, 1);
|
||||
$exp->notEq($field, $searchValue);
|
||||
} else if (strpos($searchValue, '%') !== false) {
|
||||
$exp->like($field, $searchValue);
|
||||
} else {
|
||||
$exp->eq($field, $searchValue);
|
||||
}
|
||||
return $exp;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,259 @@
|
|||
<?php
|
||||
|
||||
namespace App\Model\Behavior;
|
||||
|
||||
use Cake\ORM\Behavior;
|
||||
use Cake\ORM\Entity;
|
||||
use Cake\ORM\Query;
|
||||
use Cake\ORM\Table;
|
||||
use Cake\ORM\TableRegistry;
|
||||
use Cake\Utility\Inflector;
|
||||
use Cake\Database\Expression\QueryExpression;
|
||||
|
||||
|
||||
class MetaFieldsBehavior extends Behavior
|
||||
{
|
||||
protected $_defaultConfig = [
|
||||
'metaFieldsAssoc' => [
|
||||
'className' => 'MetaFields',
|
||||
'foreignKey' => 'parent_id',
|
||||
'bindingKey' => 'id',
|
||||
'dependent' => true,
|
||||
'cascadeCallbacks' => true,
|
||||
'saveStrategy' => 'append',
|
||||
'propertyName' => 'meta_fields',
|
||||
],
|
||||
'modelAssoc' => [
|
||||
'foreignKey' => 'parent_id',
|
||||
'bindingKey' => 'id',
|
||||
],
|
||||
'metaTemplateFieldCounter' => ['counter'],
|
||||
|
||||
'implementedEvents' => [
|
||||
'Model.beforeMarshal' => 'beforeMarshal',
|
||||
'Model.beforeFind' => 'beforeFind',
|
||||
'Model.beforeSave' => 'beforeSave',
|
||||
],
|
||||
'implementedMethods' => [
|
||||
'normalizeMetafields' => 'normalizeMetafields',
|
||||
'buildMetaFieldQuerySnippetForMatchingParent' => 'buildQuerySnippetForMatchingParent',
|
||||
],
|
||||
'implementedFinders' => [
|
||||
'metafield' => 'findMetafield',
|
||||
],
|
||||
];
|
||||
|
||||
private $aliasScope = null;
|
||||
private $typeHandlers = [];
|
||||
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
$this->bindAssociations();
|
||||
$this->_metaTemplateFieldTable = $this->_table->MetaFields->MetaTemplateFields;
|
||||
$this->_metaTemplateTable = $this->_table->MetaFields->MetaTemplates;
|
||||
$this->loadTypeHandlers();
|
||||
}
|
||||
|
||||
private function loadTypeHandlers()
|
||||
{
|
||||
if (empty($this->typeHandlers)) {
|
||||
$this->typeHandlers = $this->_metaTemplateFieldTable->getTypeHandlers();
|
||||
}
|
||||
}
|
||||
|
||||
public function getTypeHandlers(): array
|
||||
{
|
||||
return $this->typeHandlers;
|
||||
}
|
||||
|
||||
public function getScope()
|
||||
{
|
||||
if (is_null($this->aliasScope)) {
|
||||
$this->aliasScope = Inflector::underscore(Inflector::singularize($this->_table->getAlias()));
|
||||
}
|
||||
return $this->aliasScope;
|
||||
|
||||
}
|
||||
|
||||
public function bindAssociations()
|
||||
{
|
||||
$config = $this->getConfig();
|
||||
$metaFieldsAssoc = $config['metaFieldsAssoc'];
|
||||
$modelAssoc = $config['modelAssoc'];
|
||||
|
||||
$table = $this->_table;
|
||||
$tableAlias = $this->_table->getAlias();
|
||||
|
||||
$assocConditions = [
|
||||
'MetaFields.scope' => $this->getScope()
|
||||
];
|
||||
if (!$table->hasAssociation('MetaFields')) {
|
||||
$table->hasMany('MetaFields', array_merge(
|
||||
$metaFieldsAssoc,
|
||||
[
|
||||
'conditions' => $assocConditions
|
||||
]
|
||||
));
|
||||
}
|
||||
|
||||
if (!$table->MetaFields->hasAssociation($tableAlias)) {
|
||||
$table->MetaFields->belongsTo($tableAlias, array_merge(
|
||||
$modelAssoc,
|
||||
[
|
||||
'className' => get_class($table),
|
||||
]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public function beforeMarshal($event, $data, $options)
|
||||
{
|
||||
$property = $this->getConfig('metaFieldsAssoc.propertyName');
|
||||
$options['accessibleFields'][$property] = true;
|
||||
$options['associated']['MetaFields']['accessibleFields']['id'] = true;
|
||||
|
||||
if (isset($data[$property])) {
|
||||
if (!empty($data[$property])) {
|
||||
$data[$property] = $this->normalizeMetafields($data[$property]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function beforeSave($event, $entity, $options)
|
||||
{
|
||||
if (empty($entity->metaFields)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public function normalizeMetafields($metaFields)
|
||||
{
|
||||
return $metaFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
* $this->{$model}->find('metaField', [
|
||||
* ['meta_template_id' => 1, 'field' => 'email', 'value' => '%@domain.test'],
|
||||
* ['meta_template_id' => 1, 'field' => 'country_code', 'value' => '!LU'],
|
||||
* ['meta_template_id' => 1, 'field' => 'time_zone', 'value' => 'UTC+2'],
|
||||
* ])
|
||||
* $this->{$model}->find('metaField', [
|
||||
* 'AND' => [
|
||||
* ['meta_template_id' => 1, 'field' => 'email', 'value' => '%@domain.test'],
|
||||
* 'OR' => [
|
||||
* ['meta_template_id' => 1, 'field' => 'time_zone', 'value' => 'UTC+1'],
|
||||
* ['meta_template_id' => 1, 'field' => 'time_zone', 'value' => 'UTC+2'],
|
||||
* ],
|
||||
* ],
|
||||
* ])
|
||||
*/
|
||||
public function findMetafield(Query $query, array $filters)
|
||||
{
|
||||
$conditions = $this->buildQuerySnippetForMatchingParent($filters);
|
||||
$query->where($conditions);
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function buildQuerySnippetForMatchingParent(array $filters): array
|
||||
{
|
||||
if (empty($filters)) {
|
||||
return [];
|
||||
}
|
||||
if (count(array_filter(array_keys($filters), 'is_string'))) {
|
||||
$filters = [$filters];
|
||||
}
|
||||
$conjugatedFilters = $this->buildConjugatedFilters($filters);
|
||||
$conditions = $this->buildConjugatedQuerySnippet($conjugatedFilters);
|
||||
return $conditions;
|
||||
}
|
||||
|
||||
protected function buildConjugatedFilters(array $filters): array
|
||||
{
|
||||
$conjugatedFilters = [];
|
||||
foreach ($filters as $operator => $subFilters) {
|
||||
if (is_numeric($operator)) {
|
||||
$conjugatedFilters[] = $subFilters;
|
||||
} else {
|
||||
if (!empty($subFilters)) {
|
||||
$conjugatedFilters[$operator] = $this->buildConjugatedFilters($subFilters);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $conjugatedFilters;
|
||||
}
|
||||
|
||||
protected function buildConjugatedQuerySnippet(array $conjugatedFilters, string $parentOperator='AND'): array
|
||||
{
|
||||
$conditions = [];
|
||||
if (empty($conjugatedFilters['AND']) && empty($conjugatedFilters['OR'])) {
|
||||
if (count(array_filter(array_keys($conjugatedFilters), 'is_string')) > 0) {
|
||||
$conditions = $this->buildComposedQuerySnippet([$conjugatedFilters]);
|
||||
} else {
|
||||
$conditions = $this->buildComposedQuerySnippet($conjugatedFilters, $parentOperator);
|
||||
}
|
||||
} else {
|
||||
foreach ($conjugatedFilters as $subOperator => $subFilter) {
|
||||
$conditions[$subOperator] = $this->buildConjugatedQuerySnippet($subFilter, $subOperator);
|
||||
}
|
||||
}
|
||||
return $conditions;
|
||||
}
|
||||
|
||||
protected function buildComposedQuerySnippet(array $filters, string $operator='AND'): array
|
||||
{
|
||||
$conditions = [];
|
||||
foreach ($filters as $filterOperator => $filter) {
|
||||
$subQuery = $this->buildQuerySnippet($filter, true);
|
||||
$modelAlias = $this->_table->getAlias();
|
||||
$conditions[$operator][] = [$modelAlias . '.id IN' => $subQuery];
|
||||
}
|
||||
return $conditions;
|
||||
}
|
||||
|
||||
|
||||
protected function setQueryExpressionForTextField(QueryExpression $exp, string $field, string $value): QueryExpression
|
||||
{
|
||||
if (substr($value, 0, 1) == '!') {
|
||||
$value = substr($value, 1);
|
||||
$exp->notEq($field, $value);
|
||||
} else if (strpos($value, '%') !== false) {
|
||||
$exp->like($field, $value);
|
||||
} else {
|
||||
$exp->eq($field, $value);
|
||||
}
|
||||
return $exp;
|
||||
}
|
||||
|
||||
protected function buildQuerySnippet(array $filter): Query
|
||||
{
|
||||
$metaTemplateField = !empty($filter['meta_template_field_id']) ? $this->_metaTemplateFieldTable->get($filter['meta_template_field_id']) : null;
|
||||
$whereClosure = function (QueryExpression $exp) use ($filter, $metaTemplateField) {
|
||||
foreach ($filter as $column => $value) {
|
||||
$keyedColumn = 'MetaFields.' . $column;
|
||||
if ($column == 'value') {
|
||||
$this->setQueryExpressionForField($exp, $keyedColumn, $value, $metaTemplateField);
|
||||
} else {
|
||||
$this->setQueryExpressionForTextField($exp, $keyedColumn, $value);
|
||||
}
|
||||
}
|
||||
return $exp;
|
||||
};
|
||||
|
||||
$foreignKey = $this->getConfig('modelAssoc.foreignKey');
|
||||
$query = $this->_table->MetaFields->find()
|
||||
->select('MetaFields.' . $foreignKey)
|
||||
->where($whereClosure);
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function setQueryExpressionForField(QueryExpression $exp, string $field, string $value, \App\Model\Entity\MetaTemplateField $metaTemplateField=null): QueryExpression
|
||||
{
|
||||
if (!is_null($metaTemplateField) && isset($this->typeHandlers[$metaTemplateField->type])) {
|
||||
$exp = $this->typeHandlers[$metaTemplateField->type]->setQueryExpression($exp, $value, $metaTemplateField);
|
||||
} else {
|
||||
$exp = $this->setQueryExpressionForTextField($exp, $field, $value);
|
||||
}
|
||||
return $exp;
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ class Individual extends AppModel
|
|||
'created' => true,
|
||||
];
|
||||
|
||||
protected $_virtual = ['full_name'];
|
||||
protected $_virtual = ['full_name', 'alternate_emails'];
|
||||
|
||||
protected function _getFullName()
|
||||
{
|
||||
|
@ -28,4 +28,17 @@ class Individual extends AppModel
|
|||
}
|
||||
return sprintf("%s %s", $this->first_name, $this->last_name);
|
||||
}
|
||||
|
||||
protected function _getAlternateEmails()
|
||||
{
|
||||
$emails = [];
|
||||
if (!empty($this->meta_fields)) {
|
||||
foreach ($this->meta_fields as $metaField) {
|
||||
if (str_contains($metaField->field, 'email')) {
|
||||
$emails[] = $metaField;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $emails;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace App\Model\Entity;
|
||||
|
||||
use App\Model\Entity\AppModel;
|
||||
|
||||
class MailingList extends AppModel
|
||||
{
|
||||
protected $_accessible = [
|
||||
'*' => true,
|
||||
'id' => false,
|
||||
'uuid' => false,
|
||||
'user_id' => false,
|
||||
];
|
||||
|
||||
protected $_accessibleOnNew = [
|
||||
'uuid' => true,
|
||||
'user_id' => true,
|
||||
];
|
||||
|
||||
private $metaFieldsByParentId = [];
|
||||
|
||||
public function injectRegisteredEmailsIntoIndividuals()
|
||||
{
|
||||
if (empty($this->individuals)) {
|
||||
return;
|
||||
}
|
||||
if (!empty($this->meta_fields)) {
|
||||
foreach ($this->meta_fields as $meta_field) {
|
||||
$this->metaFieldsByParentId[$meta_field->parent_id][] = $meta_field;
|
||||
}
|
||||
}
|
||||
foreach ($this->individuals as $i => $individual) {
|
||||
$this->individuals[$i]->mailinglist_emails = $this->collectEmailsForMailingList($individual);
|
||||
}
|
||||
}
|
||||
|
||||
protected function collectEmailsForMailingList($individual)
|
||||
{
|
||||
$emails = [];
|
||||
if (!empty($individual['_joinData']) && !empty($individual['_joinData']['include_primary_email'])) {
|
||||
$emails[] = $individual->email;
|
||||
}
|
||||
if (!empty($this->metaFieldsByParentId[$individual->id])) {
|
||||
foreach ($this->metaFieldsByParentId[$individual->id] as $metaField) {
|
||||
$emails[] = $metaField->value;
|
||||
}
|
||||
}
|
||||
return $emails;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Model\Entity;
|
||||
|
||||
use App\Model\Entity\AppModel;
|
||||
use Cake\ORM\Entity;
|
||||
|
||||
class MetaTemplateField extends AppModel
|
||||
{
|
||||
|
||||
}
|
|
@ -7,6 +7,10 @@ use Cake\Validation\Validator;
|
|||
use Cake\Core\Configure;
|
||||
use Cake\Core\Configure\Engine\PhpConfig;
|
||||
use Cake\ORM\TableRegistry;
|
||||
use Cake\Utility\Hash;
|
||||
use Cake\Database\Expression\QueryExpression;
|
||||
use Cake\ORM\Query;
|
||||
use Cake\I18n\FrozenTime;
|
||||
|
||||
class AppTable extends Table
|
||||
{
|
||||
|
@ -14,6 +18,142 @@ class AppTable extends Table
|
|||
{
|
||||
}
|
||||
|
||||
public function getStatisticsUsageForModel(Object $table, array $scopes, array $options=[]): array
|
||||
{
|
||||
$defaultOptions = [
|
||||
'limit' => 5,
|
||||
'includeOthers' => true,
|
||||
'ignoreNull' => true,
|
||||
];
|
||||
$options = $this->getOptions($defaultOptions, $options);
|
||||
$stats = [];
|
||||
foreach ($scopes as $scope) {
|
||||
$queryTopUsage = $table->find();
|
||||
$queryTopUsage
|
||||
->select([
|
||||
$scope,
|
||||
'count' => $queryTopUsage->func()->count('id'),
|
||||
]);
|
||||
if ($queryTopUsage->getDefaultTypes()[$scope] != 'boolean') {
|
||||
$queryTopUsage->where(function (QueryExpression $exp) use ($scope) {
|
||||
return $exp
|
||||
->isNotNull($scope)
|
||||
->notEq($scope, '');
|
||||
});
|
||||
}
|
||||
$queryTopUsage
|
||||
->group($scope)
|
||||
->order(['count' => 'DESC'])
|
||||
->limit($options['limit'])
|
||||
->page(1)
|
||||
->enableHydration(false);
|
||||
$topUsage = $queryTopUsage->toList();
|
||||
$stats[$scope] = $topUsage;
|
||||
if (
|
||||
!empty($options['includeOthers']) && !empty($topUsage) &&
|
||||
$queryTopUsage->getDefaultTypes()[$scope] != 'boolean' // No need to get others as we only have 2 possibilities already considered
|
||||
) {
|
||||
$queryOthersUsage = $table->find();
|
||||
$queryOthersUsage
|
||||
->select([
|
||||
'count' => $queryOthersUsage->func()->count('id'),
|
||||
])
|
||||
->where(function (QueryExpression $exp, Query $query) use ($topUsage, $scope, $options) {
|
||||
if (!empty($options['ignoreNull'])) {
|
||||
return $exp
|
||||
->isNotNull($scope)
|
||||
->notEq($scope, '')
|
||||
->notIn($scope, Hash::extract($topUsage, "{n}.{$scope}"));
|
||||
} else {
|
||||
return $exp->or([
|
||||
$query->newExpr()->isNull($scope),
|
||||
$query->newExpr()->eq($scope, ''),
|
||||
$query->newExpr()->notIn($scope, Hash::extract($topUsage, "{n}.{$scope}")),
|
||||
]);
|
||||
}
|
||||
})
|
||||
->enableHydration(false);
|
||||
$othersUsage = $queryOthersUsage->toList();
|
||||
if (!empty($othersUsage)) {
|
||||
$stats[$scope][] = [
|
||||
$scope => __('Others'),
|
||||
'count' => $othersUsage[0]['count'],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $stats;
|
||||
}
|
||||
|
||||
private function getOptions($defaults=[], $options=[]): array
|
||||
{
|
||||
return array_merge($defaults, $options);
|
||||
}
|
||||
|
||||
// Move this into a tool
|
||||
public function getActivityStatisticsForModel(Object $table, int $days = 30): array
|
||||
{
|
||||
$statistics = [];
|
||||
if ($table->hasBehavior('Timestamp')) {
|
||||
if ($table->getSchema()->getColumnType('created') == 'datetime') {
|
||||
$statistics['created'] = $this->getActivityStatistic($table, $days, 'created');
|
||||
}
|
||||
if ($table->getSchema()->getColumnType('modified') == 'datetime') {
|
||||
$statistics['modified'] = $this->getActivityStatistic($table, $days, 'modified');
|
||||
}
|
||||
}
|
||||
return $statistics;
|
||||
}
|
||||
|
||||
public function getActivityStatistic(Object $table, int $days = 30, string $field = 'modified', bool $includeTimeline = true): array
|
||||
{
|
||||
$statistics = [];
|
||||
$statistics['days'] = $days;
|
||||
$statistics['amount'] = $table->find()->all()->count();
|
||||
if ($table->behaviors()->has('Timestamp') && $includeTimeline) {
|
||||
$statistics['timeline'] = $this->buildTimeline($table, $days, $field);
|
||||
$statistics['variation'] = $table->find()->where(["{$field} >" => FrozenTime::now()->subDays($days)])->all()->count();
|
||||
} else {
|
||||
$statistics['timeline'] = [];
|
||||
$statistics['variation'] = 0;
|
||||
}
|
||||
return $statistics;
|
||||
}
|
||||
|
||||
public function buildTimeline(Object $table, int $days = 30, string $field = 'modified'): array
|
||||
{
|
||||
$timeline = [];
|
||||
$authorizedFields = ['modified', 'created'];
|
||||
if ($table->behaviors()->has('Timestamp')) {
|
||||
if (!in_array($field, $authorizedFields)) {
|
||||
throw new MethodNotAllowedException(__('Cannot construct timeline for field `{0}`', $field));
|
||||
}
|
||||
$days = $days - 1;
|
||||
$query = $table->find();
|
||||
$query->select([
|
||||
'count' => $query->func()->count('id'),
|
||||
'date' => "DATE({$field})",
|
||||
])
|
||||
->where(["{$field} >" => FrozenTime::now()->subDays($days)])
|
||||
->group(['date'])
|
||||
->order(['date']);
|
||||
$data = $query->all()->toArray();
|
||||
$interval = new \DateInterval('P1D');
|
||||
$period = new \DatePeriod(FrozenTime::now()->subDays($days), $interval, FrozenTime::now()->addDays(1));
|
||||
foreach ($period as $date) {
|
||||
$timeline[$date->format("Y-m-d")] = [
|
||||
'time' => $date->format("Y-m-d"),
|
||||
'count' => 0
|
||||
];
|
||||
}
|
||||
foreach ($data as $entry) {
|
||||
$timeline[$entry->date]['count'] = $entry->count;
|
||||
}
|
||||
$timeline = array_values($timeline);
|
||||
}
|
||||
return $timeline;
|
||||
}
|
||||
|
||||
public function saveMetaFields($id, $input)
|
||||
{
|
||||
$this->MetaFields = TableRegistry::getTableLocator()->get('MetaFields');
|
||||
|
|
|
@ -43,14 +43,8 @@ class AuditLogsTable extends AppTable
|
|||
public function initialize(array $config): void
|
||||
{
|
||||
parent::initialize($config);
|
||||
$this->addBehavior('UUID');
|
||||
$this->addBehavior('Timestamp', [
|
||||
'Model.beoreSave' => [
|
||||
'created_at' => 'new'
|
||||
]
|
||||
]);
|
||||
$this->addBehavior('Timestamp');
|
||||
$this->belongsTo('Users');
|
||||
$this->setDisplayField('info');
|
||||
$this->compressionEnabled = Configure::read('Cerebrate.log_new_audit_compress') && function_exists('brotli_compress');
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ use Cake\ORM\RulesChecker;
|
|||
use Cake\Validation\Validator;
|
||||
use Cake\Http\Exception\NotFoundException;
|
||||
|
||||
use App\Utility\UI\Notification;
|
||||
|
||||
Type::map('json', 'Cake\Database\Type\JsonType');
|
||||
|
||||
class InboxTable extends AppTable
|
||||
|
@ -89,4 +91,44 @@ class InboxTable extends AppTable
|
|||
$savedEntry = $this->save($entryData);
|
||||
return $savedEntry;
|
||||
}
|
||||
|
||||
public function collectNotifications(\App\Model\Entity\User $user): array
|
||||
{
|
||||
$allNotifications = [];
|
||||
$inboxNotifications = $this->getNotificationsForUser($user);
|
||||
foreach ($inboxNotifications as $notification) {
|
||||
$title = __('New message');
|
||||
$details = $notification->title;
|
||||
$router = [
|
||||
'controller' => 'inbox',
|
||||
'action' => 'process',
|
||||
'plugin' => null,
|
||||
$notification->id
|
||||
];
|
||||
$allNotifications[] = (new Notification($title, $router, [
|
||||
'icon' => 'envelope',
|
||||
'details' => $details,
|
||||
'datetime' => $notification->created,
|
||||
'variant' => 'warning',
|
||||
'_useModal' => true,
|
||||
'_sidebarId' => 'inbox',
|
||||
]))->get();
|
||||
}
|
||||
return $allNotifications;
|
||||
}
|
||||
|
||||
public function getNotificationsForUser(\App\Model\Entity\User $user): array
|
||||
{
|
||||
$query = $this->find();
|
||||
$conditions = [];
|
||||
if ($user['role']['perm_admin']) {
|
||||
// Admin will not see notifications if it doesn't belong to them. They can see process the message from the inbox
|
||||
$conditions['Inbox.user_id IS'] = null;
|
||||
} else {
|
||||
$conditions['Inbox.user_id'] = $user->id;
|
||||
}
|
||||
$query->where($conditions);
|
||||
$notifications = $query->all()->toArray();
|
||||
return $notifications;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,15 +10,15 @@ use Cake\ORM\Query;
|
|||
|
||||
class IndividualsTable extends AppTable
|
||||
{
|
||||
public $metaFields = 'individual';
|
||||
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
parent::initialize($config);
|
||||
$this->addBehavior('UUID');
|
||||
$this->addBehavior('Timestamp');
|
||||
$this->addBehavior('Tags.Tag');
|
||||
$this->addBehavior('MetaFields');
|
||||
$this->addBehavior('AuditLog');
|
||||
|
||||
$this->hasMany(
|
||||
'Alignments',
|
||||
[
|
||||
|
@ -39,6 +39,10 @@ class IndividualsTable extends AppTable
|
|||
$this->belongsToMany('Organisations', [
|
||||
'through' => 'Alignments',
|
||||
]);
|
||||
$this->belongsToMany('MailingLists', [
|
||||
'through' => 'mailing_lists_individuals',
|
||||
]);
|
||||
|
||||
$this->setDisplayField('email');
|
||||
}
|
||||
|
||||
|
|
|
@ -9,18 +9,18 @@ use Cake\Validation\Validator;
|
|||
use Migrations\Migrations;
|
||||
use Cake\Filesystem\Folder;
|
||||
use Cake\Http\Exception\MethodNotAllowedException;
|
||||
use Cake\I18n\FrozenTime;
|
||||
|
||||
class InstanceTable extends AppTable
|
||||
{
|
||||
protected $activePlugins = ['Tags', 'ADmad/SocialAuth'];
|
||||
public $seachAllTables = ['Broods', 'Individuals', 'Organisations', 'SharingGroups', 'Users', 'EncryptionKeys', ];
|
||||
public $seachAllTables = [];
|
||||
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
parent::initialize($config);
|
||||
$this->addBehavior('AuditLog');
|
||||
$this->setDisplayField('name');
|
||||
$this->setSearchAllTables();
|
||||
}
|
||||
|
||||
public function validationDefault(Validator $validator): Validator
|
||||
|
@ -28,88 +28,121 @@ class InstanceTable extends AppTable
|
|||
return $validator;
|
||||
}
|
||||
|
||||
public function getStatistics($days=30): array
|
||||
public function setSearchAllTables(): void
|
||||
{
|
||||
$this->seachAllTables = [
|
||||
'Broods' => ['conditions' => false, 'afterFind' => false],
|
||||
'Individuals' => ['conditions' => false, 'afterFind' => false],
|
||||
'Organisations' => ['conditions' => false, 'afterFind' => false],
|
||||
'SharingGroups' => [
|
||||
'conditions' => false,
|
||||
'afterFind' => function($result, $user) {
|
||||
foreach ($result as $i => $row) {
|
||||
if (empty($user['role']['perm_admin'])) {
|
||||
$orgFound = false;
|
||||
if (!empty($row['sharing_group_orgs'])) {
|
||||
foreach ($row['sharing_group_orgs'] as $org) {
|
||||
if ($org['id'] === $user['organisation_id']) {
|
||||
$orgFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($row['organisation_id'] !== $user['organisation_id'] && !$orgFound) {
|
||||
unset($result[$i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
},
|
||||
],
|
||||
'Users' => [
|
||||
'conditions' => function($user) {
|
||||
$conditions = [];
|
||||
if (empty($user['role']['perm_admin'])) {
|
||||
$conditions['Users.organisation_id'] = $user['organisation_id'];
|
||||
}
|
||||
return $conditions;
|
||||
},
|
||||
'afterFind' => function ($result, $user) {
|
||||
return $result;
|
||||
},
|
||||
],
|
||||
'EncryptionKeys' => ['conditions' => false, 'afterFind' => false],
|
||||
];
|
||||
}
|
||||
|
||||
public function getStatistics(int $days=30): array
|
||||
{
|
||||
$models = ['Individuals', 'Organisations', 'Alignments', 'EncryptionKeys', 'SharingGroups', 'Users', 'Broods', 'Tags.Tags'];
|
||||
foreach ($models as $model) {
|
||||
$table = TableRegistry::getTableLocator()->get($model);
|
||||
$statistics[$model]['amount'] = $table->find()->all()->count();
|
||||
if ($table->behaviors()->has('Timestamp')) {
|
||||
$query = $table->find();
|
||||
$query->select([
|
||||
'count' => $query->func()->count('id'),
|
||||
'date' => 'DATE(modified)',
|
||||
])
|
||||
->where(['modified >' => FrozenTime::now()->subDays($days)])
|
||||
->group(['date'])
|
||||
->order(['date']);
|
||||
$data = $query->toArray();
|
||||
$interval = new \DateInterval('P1D');
|
||||
$period = new \DatePeriod(FrozenTime::now()->subDays($days), $interval, FrozenTime::now());
|
||||
$timeline = [];
|
||||
foreach ($period as $date) {
|
||||
$timeline[$date->format("Y-m-d")] = [
|
||||
'time' => $date->format("Y-m-d"),
|
||||
'count' => 0
|
||||
];
|
||||
}
|
||||
foreach ($data as $entry) {
|
||||
$timeline[$entry->date]['count'] = $entry->count;
|
||||
}
|
||||
$statistics[$model]['timeline'] = array_values($timeline);
|
||||
|
||||
$startCount = $table->find()->where(['modified <' => FrozenTime::now()->subDays($days)])->all()->count();
|
||||
$endCount = $statistics[$model]['amount'];
|
||||
$statistics[$model]['variation'] = $endCount - $startCount;
|
||||
} else {
|
||||
$statistics[$model]['timeline'] = [];
|
||||
$statistics[$model]['variation'] = 0;
|
||||
}
|
||||
$statistics[$model] = $this->getActivityStatisticsForModel($table, $days);
|
||||
}
|
||||
return $statistics;
|
||||
}
|
||||
|
||||
public function searchAll($value, $limit=5, $model=null)
|
||||
public function searchAll($value, $user, $limit=5, $model=null)
|
||||
{
|
||||
$results = [];
|
||||
|
||||
// search in metafields. FIXME: To be replaced by the meta-template system
|
||||
$metaFieldTable = TableRegistry::get('MetaFields');
|
||||
$query = $metaFieldTable->find()->where([
|
||||
'value LIKE' => '%' . $value . '%'
|
||||
]);
|
||||
$results['MetaFields']['amount'] = $query->count();
|
||||
$result = $query->limit($limit)->all()->toList();
|
||||
if (!empty($result)) {
|
||||
$results['MetaFields']['entries'] = $result;
|
||||
}
|
||||
|
||||
$models = $this->seachAllTables;
|
||||
if (!is_null($model)) {
|
||||
if (in_array($model, $this->seachAllTables)) {
|
||||
$models = [$model];
|
||||
if (in_array($model, array_keys($this->seachAllTables))) {
|
||||
$models = [$model => $this->seachAllTables[$model]];
|
||||
} else {
|
||||
return $results; // Cannot search in this model
|
||||
}
|
||||
}
|
||||
foreach ($models as $tableName) {
|
||||
|
||||
// search in metafields. FIXME?: Use meta-fields type handler to search for meta-field values
|
||||
if (is_null($model)) {
|
||||
$metaFieldTable = TableRegistry::get('MetaFields');
|
||||
$query = $metaFieldTable->find()->where([
|
||||
'value LIKE' => '%' . $value . '%'
|
||||
]);
|
||||
$results['MetaFields']['amount'] = $query->count();
|
||||
$result = $query->limit($limit)->all()->toList();
|
||||
if (!empty($result)) {
|
||||
$results['MetaFields']['entries'] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($models as $tableName => $tableConfig) {
|
||||
$controller = $this->getController($tableName);
|
||||
$table = TableRegistry::get($tableName);
|
||||
$query = $table->find();
|
||||
$quickFilterOptions = $this->getQuickFiltersFieldsFromController($controller);
|
||||
$quickFilters = $this->getQuickFiltersFieldsFromController($controller);
|
||||
$containFields = $this->getContainFieldsFromController($controller);
|
||||
if (empty($quickFilterOptions)) {
|
||||
if (empty($quickFilters)) {
|
||||
continue; // make sure we are filtering on something
|
||||
}
|
||||
$params = ['quickFilter' => $value];
|
||||
$quickFilterOptions = ['quickFilters' => $quickFilters];
|
||||
$query = $controller->CRUD->setQuickFilters($params, $query, $quickFilterOptions);
|
||||
if (!empty($tableConfig['conditions'])) {
|
||||
$whereClause = [];
|
||||
if (is_callable($tableConfig['conditions'])) {
|
||||
$whereClause = $tableConfig['conditions']($user);
|
||||
} else {
|
||||
$whereClause = $tableConfig['conditions'];
|
||||
}
|
||||
$query->where($whereClause);
|
||||
}
|
||||
if (!empty($containFields)) {
|
||||
$query->contain($containFields);
|
||||
}
|
||||
$results[$tableName]['amount'] = $query->count();
|
||||
if (!empty($tableConfig['contain'])) {
|
||||
$query->contain($tableConfig['contain']);
|
||||
}
|
||||
if (empty($tableConfig['afterFind'])) {
|
||||
$results[$tableName]['amount'] = $query->count();
|
||||
}
|
||||
$result = $query->limit($limit)->all()->toList();
|
||||
if (!empty($result)) {
|
||||
if (!empty($tableConfig['afterFind'])) {
|
||||
$result = $tableConfig['afterFind']($result, $user);
|
||||
}
|
||||
$results[$tableName]['entries'] = $result;
|
||||
$results[$tableName]['amount'] = count($result);
|
||||
}
|
||||
}
|
||||
return $results;
|
||||
|
@ -119,7 +152,7 @@ class InstanceTable extends AppTable
|
|||
{
|
||||
$controllerName = "\\App\\Controller\\{$name}Controller";
|
||||
if (!class_exists($controllerName)) {
|
||||
throw new MethodNotAllowedException(__('Model `{0}` does not exists', $model));
|
||||
throw new MethodNotAllowedException(__('Model `{0}` does not exists', $name));
|
||||
}
|
||||
$controller = new $controllerName;
|
||||
return $controller;
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace App\Model\Table;
|
||||
|
||||
use App\Model\Table\AppTable;
|
||||
use Cake\ORM\Table;
|
||||
use Cake\Validation\Validator;
|
||||
use Cake\ORM\RulesChecker;
|
||||
use Cake\ORM\TableRegistry;
|
||||
|
||||
class MailingListsTable extends AppTable
|
||||
{
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
parent::initialize($config);
|
||||
$this->addBehavior('UUID');
|
||||
$this->addBehavior('Timestamp');
|
||||
$this->belongsTo(
|
||||
'Users'
|
||||
);
|
||||
|
||||
$this->belongsToMany('Individuals', [
|
||||
'joinTable' => 'mailing_lists_individuals',
|
||||
]);
|
||||
// Change to HasMany?
|
||||
$this->belongsToMany('MetaFields');
|
||||
|
||||
$this->setDisplayField('name');
|
||||
}
|
||||
|
||||
public function validationDefault(Validator $validator): Validator
|
||||
{
|
||||
$validator
|
||||
->requirePresence(['name', 'releasability'], 'create');
|
||||
return $validator;
|
||||
}
|
||||
|
||||
public function buildRules(RulesChecker $rules): RulesChecker
|
||||
{
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function isIndividualListed($individual, \App\Model\Entity\MailingList $mailinglist): bool
|
||||
{
|
||||
$found = false;
|
||||
if (empty($mailinglist['individuals'])) {
|
||||
return false;
|
||||
}
|
||||
$individual_id_to_find = $individual;
|
||||
if (is_object($individual)) {
|
||||
$individual_id_to_find = $individual['id'];
|
||||
}
|
||||
foreach ($mailinglist['individuals'] as $individual) {
|
||||
if ($individual['id'] == $individual_id_to_find) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return $found;
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ namespace App\Model\Table;
|
|||
use App\Model\Table\AppTable;
|
||||
use Cake\ORM\Table;
|
||||
use Cake\Validation\Validator;
|
||||
use Cake\ORM\RulesChecker;
|
||||
|
||||
class MetaFieldsTable extends AppTable
|
||||
{
|
||||
|
@ -12,11 +13,17 @@ class MetaFieldsTable extends AppTable
|
|||
{
|
||||
parent::initialize($config);
|
||||
$this->addBehavior('UUID');
|
||||
$this->addBehavior('Timestamp');
|
||||
$this->addBehavior('CounterCache', [
|
||||
'MetaTemplateFields' => ['counter']
|
||||
]);
|
||||
|
||||
$this->addBehavior('AuditLog');
|
||||
$this->addBehavior('Timestamp');
|
||||
$this->setDisplayField('field');
|
||||
$this->belongsTo('MetaTemplates');
|
||||
$this->belongsTo('MetaTemplateFields');
|
||||
|
||||
$this->setDisplayField('field');
|
||||
}
|
||||
|
||||
public function validationDefault(Validator $validator): Validator
|
||||
|
@ -30,7 +37,59 @@ class MetaFieldsTable extends AppTable
|
|||
->notEmptyString('meta_template_field_id')
|
||||
->requirePresence(['scope', 'field', 'value', 'uuid', 'meta_template_id', 'meta_template_field_id'], 'create');
|
||||
|
||||
// add validation regex
|
||||
$validator->add('value', 'validMetaField', [
|
||||
'rule' => 'isValidMetaField',
|
||||
'message' => __('The provided value doesn\'t pass the validation check for its meta-template.'),
|
||||
'provider' => 'table',
|
||||
]);
|
||||
|
||||
return $validator;
|
||||
}
|
||||
|
||||
public function isValidMetaField($value, array $context)
|
||||
{
|
||||
$metaFieldsTable = $context['providers']['table'];
|
||||
$entityData = $context['data'];
|
||||
$metaTemplateField = $metaFieldsTable->MetaTemplateFields->get($entityData['meta_template_field_id']);
|
||||
return $this->isValidMetaFieldForMetaTemplateField($value, $metaTemplateField);
|
||||
}
|
||||
|
||||
public function isValidMetaFieldForMetaTemplateField($value, $metaTemplateField)
|
||||
{
|
||||
$typeValid = $this->isValidType($value, $metaTemplateField);
|
||||
if ($typeValid !== true) {
|
||||
return $typeValid;
|
||||
}
|
||||
if (!empty($metaTemplateField['regex'])) {
|
||||
return $this->isValidRegex($value, $metaTemplateField);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isValidType($value, $metaTemplateField)
|
||||
{
|
||||
if (empty($value)) {
|
||||
return __('Metafield value cannot be empty.');
|
||||
}
|
||||
$typeHandler = $this->MetaTemplateFields->getTypeHandler($metaTemplateField['type']);
|
||||
if (!empty($typeHandler)) {
|
||||
$success = $typeHandler->validate($value);
|
||||
return $success ? true : __('Metafields value `{0}` for `{1}` doesn\'t pass type validation.', $value, $metaTemplateField['field']);
|
||||
}
|
||||
/*
|
||||
We should not end-up in this case. But if someone creates a new type without his handler,
|
||||
we consider its type to be a valid text.
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isValidRegex($value, $metaTemplateField)
|
||||
{
|
||||
|
||||
$re = $metaTemplateField['regex'];
|
||||
if (!preg_match("/^$re$/m", $value)) {
|
||||
return __('Metafield value `{0}` for `{1}` doesn\'t pass regex validation.', $value, $metaTemplateField['field']);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,16 +6,37 @@ use App\Model\Table\AppTable;
|
|||
use Cake\ORM\Table;
|
||||
use Cake\Validation\Validator;
|
||||
|
||||
use MetaFieldsTypes\TextType;
|
||||
use MetaFieldsTypes\IPv4Type;
|
||||
use MetaFieldsTypes\IPv6Type;
|
||||
require_once(APP . 'Lib' . DS . 'default' . DS . 'meta_fields_types' . DS . 'TextType.php');
|
||||
require_once(APP . 'Lib' . DS . 'default' . DS . 'meta_fields_types' . DS . 'IPv4Type.php');
|
||||
require_once(APP . 'Lib' . DS . 'default' . DS . 'meta_fields_types' . DS . 'IPv6Type.php');
|
||||
|
||||
class MetaTemplateFieldsTable extends AppTable
|
||||
{
|
||||
private $typeHandlers = [];
|
||||
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
parent::initialize($config);
|
||||
|
||||
$this->BelongsTo(
|
||||
'MetaTemplates'
|
||||
);
|
||||
$this->hasMany('MetaFields');
|
||||
|
||||
$this->setDisplayField('field');
|
||||
$this->loadTypeHandlers();
|
||||
}
|
||||
|
||||
public function beforeSave($event, $entity, $options)
|
||||
{
|
||||
if (empty($entity->meta_template_id)) {
|
||||
$event->stopPropagation();
|
||||
$event->setResult(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public function validationDefault(Validator $validator): Validator
|
||||
|
@ -23,9 +44,31 @@ class MetaTemplateFieldsTable extends AppTable
|
|||
$validator
|
||||
->notEmptyString('field')
|
||||
->notEmptyString('type')
|
||||
->numeric('meta_template_id')
|
||||
->notBlank('meta_template_id')
|
||||
->requirePresence(['meta_template_id', 'field', 'type'], 'create');
|
||||
->requirePresence(['field', 'type'], 'create');
|
||||
return $validator;
|
||||
}
|
||||
|
||||
public function loadTypeHandlers(): void
|
||||
{
|
||||
if (empty($this->typeHandlers)) {
|
||||
$typeHandlers = [
|
||||
new TextType(),
|
||||
new IPv4Type(),
|
||||
new IPv6Type(),
|
||||
];
|
||||
foreach ($typeHandlers as $handler) {
|
||||
$this->typeHandlers[$handler::TYPE] = $handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getTypeHandlers(): array
|
||||
{
|
||||
return $this->typeHandlers;
|
||||
}
|
||||
|
||||
public function getTypeHandler($type)
|
||||
{
|
||||
return $this->typeHandlers[$type] ?? false;
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -9,8 +9,6 @@ use Cake\Error\Debugger;
|
|||
|
||||
class OrganisationsTable extends AppTable
|
||||
{
|
||||
public $metaFields = 'organisation';
|
||||
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
parent::initialize($config);
|
||||
|
@ -33,14 +31,7 @@ class OrganisationsTable extends AppTable
|
|||
'conditions' => ['owner_model' => 'organisation']
|
||||
]
|
||||
);
|
||||
$this->hasMany(
|
||||
'MetaFields',
|
||||
[
|
||||
'dependent' => true,
|
||||
'foreignKey' => 'parent_id',
|
||||
'conditions' => ['MetaFields.scope' => 'organisation']
|
||||
]
|
||||
);
|
||||
$this->addBehavior('MetaFields');
|
||||
$this->setDisplayField('name');
|
||||
}
|
||||
|
||||
|
|
|
@ -153,7 +153,7 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
|
|||
'Providers' => [
|
||||
'PasswordAuth' => [
|
||||
'password_auth.enabled' => [
|
||||
'name' => 'Disable password authentication',
|
||||
'name' => 'Enable password authentication',
|
||||
'type' => 'boolean',
|
||||
'severity' => 'warning',
|
||||
'description' => __('Enable username/password authentication.'),
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Utility\UI;
|
||||
|
||||
use Cake\Utility\Inflector;
|
||||
|
||||
class IndexSetting
|
||||
{
|
||||
public static function getAllSetting($user): array
|
||||
{
|
||||
$rawSetting = !empty($user->user_settings_by_name['ui.table_setting']['value']) ? json_decode($user->user_settings_by_name['ui.table_setting']['value'], true) : [];
|
||||
return $rawSetting;
|
||||
}
|
||||
|
||||
public static function getTableSetting($user, $tableId): array
|
||||
{
|
||||
$rawSetting = IndexSetting::getAllSetting($user);
|
||||
if (is_object($tableId)) {
|
||||
$tableId = IndexSetting::getIDFromTable($tableId);
|
||||
}
|
||||
$tableSettings = !empty($rawSetting[$tableId]) ? $rawSetting[$tableId] : [];
|
||||
return $tableSettings;
|
||||
}
|
||||
|
||||
public static function getIDFromTable(Object $table): string
|
||||
{
|
||||
return sprintf('%s_index', Inflector::variable(Inflector::singularize(($table->getAlias()))));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Utility\UI;
|
||||
|
||||
use Cake\Validation\Validator;
|
||||
|
||||
|
||||
class Notification
|
||||
{
|
||||
public $text = '';
|
||||
public $router = null;
|
||||
public $details = null;
|
||||
public $icon = 'exclamation-circle';
|
||||
public $variant = 'primary';
|
||||
public $datetime = null;
|
||||
public $_useModal = false;
|
||||
public $_sidebarId = null;
|
||||
|
||||
|
||||
public function __construct(string $text, array $router, $options = [])
|
||||
{
|
||||
$this->text = $text;
|
||||
$this->router = $router;
|
||||
foreach ($options as $key => $value) {
|
||||
$this->{$key} = $value;
|
||||
}
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
public function get(): array
|
||||
{
|
||||
if (empty($errors)) {
|
||||
return (array) $this;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function validate()
|
||||
{
|
||||
$validator = new Validator();
|
||||
|
||||
$validator
|
||||
->requirePresence('title', 'create')
|
||||
->notEmptyString('title');
|
||||
|
||||
return $validator->validate((array) $this);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Bootstrap Tabs helper
|
||||
* Options:
|
||||
|
@ -81,7 +82,7 @@ class BootstrapHelper extends Helper
|
|||
return $bsButton->button();
|
||||
}
|
||||
|
||||
public function icon($icon, $options=[])
|
||||
public function icon($icon, $options = [])
|
||||
{
|
||||
$bsIcon = new BoostrapIcon($icon, $options);
|
||||
return $bsIcon->icon();
|
||||
|
@ -98,7 +99,7 @@ class BootstrapHelper extends Helper
|
|||
$bsModal = new BoostrapModal($options);
|
||||
return $bsModal->modal();
|
||||
}
|
||||
|
||||
|
||||
public function card($options)
|
||||
{
|
||||
$bsCard = new BoostrapCard($options);
|
||||
|
@ -117,6 +118,12 @@ class BootstrapHelper extends Helper
|
|||
return $bsCollapse->collapse();
|
||||
}
|
||||
|
||||
public function accordion($options, $content)
|
||||
{
|
||||
$bsAccordion = new BoostrapAccordion($options, $content, $this);
|
||||
return $bsAccordion->accordion();
|
||||
}
|
||||
|
||||
public function progressTimeline($options)
|
||||
{
|
||||
$bsProgressTimeline = new BoostrapProgressTimeline($options, $this);
|
||||
|
@ -129,7 +136,7 @@ class BootstrapHelper extends Helper
|
|||
return $bsListGroup->listGroup();
|
||||
}
|
||||
|
||||
public function genNode($node, $params=[], $content='')
|
||||
public function genNode($node, $params = [], $content = '')
|
||||
{
|
||||
return BootstrapGeneric::genNode($node, $params, $content);
|
||||
}
|
||||
|
@ -140,6 +147,12 @@ class BootstrapHelper extends Helper
|
|||
return $bsSwitch->switch();
|
||||
}
|
||||
|
||||
public function notificationBubble($options)
|
||||
{
|
||||
$bsNotificationBubble = new BoostrapNotificationBuble($options, $this);
|
||||
return $bsNotificationBubble->notificationBubble();
|
||||
}
|
||||
|
||||
public function dropdownMenu($options)
|
||||
{
|
||||
$bsDropdownMenu = new BoostrapDropdownMenu($options, $this);
|
||||
|
@ -177,12 +190,12 @@ class BootstrapGeneric
|
|||
}
|
||||
}
|
||||
|
||||
public static function genNode($node, $params=[], $content="")
|
||||
public static function genNode($node, $params = [], $content = "")
|
||||
{
|
||||
return sprintf('<%s %s>%s</%s>', $node, BootstrapGeneric::genHTMLParams($params), $content, $node);
|
||||
}
|
||||
|
||||
protected static function openNode($node, $params=[])
|
||||
protected static function openNode($node, $params = [])
|
||||
{
|
||||
return sprintf('<%s %s>', $node, BootstrapGeneric::genHTMLParams($params));
|
||||
}
|
||||
|
@ -249,7 +262,8 @@ class BootstrapTabs extends BootstrapGeneric
|
|||
];
|
||||
private $bsClasses = null;
|
||||
|
||||
function __construct($options) {
|
||||
function __construct($options)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'justify' => [false, 'center', 'end'],
|
||||
'body-variant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||
|
@ -284,7 +298,7 @@ class BootstrapTabs extends BootstrapGeneric
|
|||
$this->options['pills'] = true;
|
||||
$this->options['card'] = true;
|
||||
}
|
||||
|
||||
|
||||
if ($this->options['pills']) {
|
||||
$this->bsClasses['nav'][] = 'nav-pills';
|
||||
if ($this->options['vertical']) {
|
||||
|
@ -307,7 +321,7 @@ class BootstrapTabs extends BootstrapGeneric
|
|||
$this->bsClasses['nav'][] = 'nav-justify';
|
||||
}
|
||||
|
||||
$activeTab = 0;
|
||||
$activeTab = array_key_first($this->data['navs']);
|
||||
foreach ($this->data['navs'] as $i => $nav) {
|
||||
if (!is_array($nav)) {
|
||||
$this->data['navs'][$i] = ['text' => $nav];
|
||||
|
@ -386,29 +400,31 @@ class BootstrapTabs extends BootstrapGeneric
|
|||
"border-{$this->options['header-border-variant']}"
|
||||
]
|
||||
)]);
|
||||
$html .= $this->openNode('div', ['class' => array_merge(
|
||||
[
|
||||
($this->options['vertical-size'] != 'auto' ? 'col-' . $this->options['vertical-size'] : ''),
|
||||
($this->options['card'] ? 'card-header border-end' : '')
|
||||
],
|
||||
[
|
||||
"bg-{$this->options['header-variant']}",
|
||||
"text-{$this->options['header-text-variant']}",
|
||||
"border-{$this->options['header-border-variant']}"
|
||||
])]);
|
||||
$html .= $this->genNav();
|
||||
$html .= $this->closeNode('div');
|
||||
$html .= $this->openNode('div', ['class' => array_merge(
|
||||
[
|
||||
($this->options['vertical-size'] != 'auto' ? 'col-' . (12 - $this->options['vertical-size']) : ''),
|
||||
($this->options['card'] ? 'card-body2' : '')
|
||||
],
|
||||
[
|
||||
"bg-{$this->options['body-variant']}",
|
||||
"text-{$this->options['body-text-variant']}"
|
||||
])]);
|
||||
$html .= $this->genContent();
|
||||
$html .= $this->closeNode('div');
|
||||
$html .= $this->openNode('div', ['class' => array_merge(
|
||||
[
|
||||
($this->options['vertical-size'] != 'auto' ? 'col-' . $this->options['vertical-size'] : ''),
|
||||
($this->options['card'] ? 'card-header border-end' : '')
|
||||
],
|
||||
[
|
||||
"bg-{$this->options['header-variant']}",
|
||||
"text-{$this->options['header-text-variant']}",
|
||||
"border-{$this->options['header-border-variant']}"
|
||||
]
|
||||
)]);
|
||||
$html .= $this->genNav();
|
||||
$html .= $this->closeNode('div');
|
||||
$html .= $this->openNode('div', ['class' => array_merge(
|
||||
[
|
||||
($this->options['vertical-size'] != 'auto' ? 'col-' . (12 - $this->options['vertical-size']) : ''),
|
||||
($this->options['card'] ? 'card-body2' : '')
|
||||
],
|
||||
[
|
||||
"bg-{$this->options['body-variant']}",
|
||||
"text-{$this->options['body-text-variant']}"
|
||||
]
|
||||
)]);
|
||||
$html .= $this->genContent();
|
||||
$html .= $this->closeNode('div');
|
||||
$html .= $this->closeNode('div');
|
||||
return $html;
|
||||
}
|
||||
|
@ -479,7 +495,8 @@ class BootstrapTabs extends BootstrapGeneric
|
|||
}
|
||||
}
|
||||
|
||||
class BoostrapAlert extends BootstrapGeneric {
|
||||
class BoostrapAlert extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'text' => '',
|
||||
'html' => null,
|
||||
|
@ -490,7 +507,8 @@ class BoostrapAlert extends BootstrapGeneric {
|
|||
|
||||
private $bsClasses = null;
|
||||
|
||||
function __construct($options) {
|
||||
function __construct($options)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => BootstrapGeneric::$variants,
|
||||
];
|
||||
|
@ -537,11 +555,12 @@ class BoostrapAlert extends BootstrapGeneric {
|
|||
|
||||
private function genContent()
|
||||
{
|
||||
return !is_null($this->options['html']) ? $this->options['html'] : $this->options['text'];
|
||||
return !is_null($this->options['html']) ? $this->options['html'] : h($this->options['text']);
|
||||
}
|
||||
}
|
||||
|
||||
class BoostrapTable extends BootstrapGeneric {
|
||||
class BoostrapTable extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'striped' => true,
|
||||
'bordered' => true,
|
||||
|
@ -556,7 +575,8 @@ class BoostrapTable extends BootstrapGeneric {
|
|||
|
||||
private $bsClasses = null;
|
||||
|
||||
function __construct($options, $data, $btHelper) {
|
||||
function __construct($options, $data, $btHelper)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => array_merge(BootstrapGeneric::$variants, [''])
|
||||
];
|
||||
|
@ -597,7 +617,7 @@ class BoostrapTable extends BootstrapGeneric {
|
|||
$html .= $this->genCaption();
|
||||
$html .= $this->genHeader();
|
||||
$html .= $this->genBody();
|
||||
|
||||
|
||||
$html .= $this->closeNode('table');
|
||||
return $html;
|
||||
}
|
||||
|
@ -645,7 +665,7 @@ class BoostrapTable extends BootstrapGeneric {
|
|||
|
||||
private function genRow($row, $rowIndex)
|
||||
{
|
||||
$html = $this->openNode('tr',[
|
||||
$html = $this->openNode('tr', [
|
||||
'class' => [
|
||||
!empty($row['_rowVariant']) ? "table-{$row['_rowVariant']}" : ''
|
||||
]
|
||||
|
@ -694,7 +714,8 @@ class BoostrapTable extends BootstrapGeneric {
|
|||
}
|
||||
}
|
||||
|
||||
class BoostrapListTable extends BootstrapGeneric {
|
||||
class BoostrapListTable extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'striped' => true,
|
||||
'bordered' => false,
|
||||
|
@ -708,7 +729,8 @@ class BoostrapListTable extends BootstrapGeneric {
|
|||
|
||||
private $bsClasses = null;
|
||||
|
||||
function __construct($options, $data, $btHelper) {
|
||||
function __construct($options, $data, $btHelper)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => array_merge(BootstrapGeneric::$variants, [''])
|
||||
];
|
||||
|
@ -749,7 +771,7 @@ class BoostrapListTable extends BootstrapGeneric {
|
|||
|
||||
$html .= $this->genCaption();
|
||||
$html .= $this->genBody();
|
||||
|
||||
|
||||
$html .= $this->closeNode('table');
|
||||
return $html;
|
||||
}
|
||||
|
@ -773,11 +795,11 @@ class BoostrapListTable extends BootstrapGeneric {
|
|||
$rowValue = $this->genCell($field);
|
||||
$rowKey = $this->genNode('th', [
|
||||
'class' => [
|
||||
'col-sm-2'
|
||||
'col-4 col-sm-2'
|
||||
],
|
||||
'scope' => 'row'
|
||||
], h($field['key']));
|
||||
$row = $this->genNode('tr',[
|
||||
$row = $this->genNode('tr', [
|
||||
'class' => [
|
||||
'd-flex',
|
||||
!empty($field['_rowVariant']) ? "table-{$field['_rowVariant']}" : ''
|
||||
|
@ -786,7 +808,7 @@ class BoostrapListTable extends BootstrapGeneric {
|
|||
return $row;
|
||||
}
|
||||
|
||||
private function genCell($field=[])
|
||||
private function genCell($field = [])
|
||||
{
|
||||
if (isset($field['raw'])) {
|
||||
$cellContent = h($field['raw']);
|
||||
|
@ -802,7 +824,7 @@ class BoostrapListTable extends BootstrapGeneric {
|
|||
}
|
||||
return $this->genNode('td', [
|
||||
'class' => [
|
||||
'col-sm-10',
|
||||
'col-8 col-sm-10',
|
||||
!empty($field['_cellVariant']) ? "bg-{$field['_cellVariant']}" : ''
|
||||
]
|
||||
], $cellContent);
|
||||
|
@ -821,7 +843,8 @@ class BoostrapListTable extends BootstrapGeneric {
|
|||
|
||||
private function getElementPath($type)
|
||||
{
|
||||
return sprintf('%s%sField',
|
||||
return sprintf(
|
||||
'%s%sField',
|
||||
$this->options['elementsRootPath'] ?? '',
|
||||
$type
|
||||
);
|
||||
|
@ -833,7 +856,8 @@ class BoostrapListTable extends BootstrapGeneric {
|
|||
}
|
||||
}
|
||||
|
||||
class BoostrapButton extends BootstrapGeneric {
|
||||
class BoostrapButton extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'id' => '',
|
||||
'text' => '',
|
||||
|
@ -853,10 +877,11 @@ class BoostrapButton extends BootstrapGeneric {
|
|||
|
||||
private $bsClasses = [];
|
||||
|
||||
function __construct($options) {
|
||||
function __construct($options)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => array_merge(BootstrapGeneric::$variants, ['link', 'text']),
|
||||
'size' => ['', 'sm', 'lg'],
|
||||
'size' => ['', 'xs', 'sm', 'lg'],
|
||||
'type' => ['button', 'submit', 'reset']
|
||||
];
|
||||
if (empty($options['class'])) {
|
||||
|
@ -871,6 +896,10 @@ class BoostrapButton extends BootstrapGeneric {
|
|||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->checkOptionValidity();
|
||||
|
||||
if (!empty($this->options['id'])) {
|
||||
$this->options['params']['id'] = $this->options['id'];
|
||||
}
|
||||
|
||||
$this->bsClasses[] = 'btn';
|
||||
if ($this->options['outline']) {
|
||||
$this->bsClasses[] = "btn-outline-{$this->options['variant']}";
|
||||
|
@ -942,7 +971,8 @@ class BoostrapButton extends BootstrapGeneric {
|
|||
}
|
||||
}
|
||||
|
||||
class BoostrapBadge extends BootstrapGeneric {
|
||||
class BoostrapBadge extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'text' => '',
|
||||
'variant' => 'primary',
|
||||
|
@ -951,7 +981,8 @@ class BoostrapBadge extends BootstrapGeneric {
|
|||
'class' => [],
|
||||
];
|
||||
|
||||
function __construct($options) {
|
||||
function __construct($options)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => BootstrapGeneric::$variants,
|
||||
];
|
||||
|
@ -961,6 +992,7 @@ class BoostrapBadge extends BootstrapGeneric {
|
|||
private function processOptions($options)
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->options['class'] = is_array($this->options['class']) ? $this->options['class'] : [$this->options['class']];
|
||||
$this->checkOptionValidity();
|
||||
}
|
||||
|
||||
|
@ -985,13 +1017,17 @@ class BoostrapBadge extends BootstrapGeneric {
|
|||
}
|
||||
}
|
||||
|
||||
class BoostrapIcon extends BootstrapGeneric {
|
||||
class BoostrapIcon extends BootstrapGeneric
|
||||
{
|
||||
private $icon = '';
|
||||
private $defaultOptions = [
|
||||
'class' => [],
|
||||
'title' => '',
|
||||
'params' => [],
|
||||
];
|
||||
|
||||
function __construct($icon, $options=[]) {
|
||||
function __construct($icon, $options = [])
|
||||
{
|
||||
$this->icon = $icon;
|
||||
$this->processOptions($options);
|
||||
}
|
||||
|
@ -1009,17 +1045,19 @@ class BoostrapIcon extends BootstrapGeneric {
|
|||
|
||||
private function genIcon()
|
||||
{
|
||||
$html = $this->genNode('span', [
|
||||
$html = $this->genNode('span', array_merge([
|
||||
'class' => array_merge(
|
||||
is_array($this->options['class']) ? $this->options['class'] : [$this->options['class']],
|
||||
["fa fa-{$this->icon}"]
|
||||
),
|
||||
]);
|
||||
'title' => h($this->options['title'])
|
||||
], $this->options['params']));
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
class BoostrapModal extends BootstrapGeneric {
|
||||
class BoostrapModal extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'size' => '',
|
||||
'centered' => true,
|
||||
|
@ -1044,10 +1082,11 @@ class BoostrapModal extends BootstrapGeneric {
|
|||
|
||||
private $bsClasses = null;
|
||||
|
||||
function __construct($options) {
|
||||
function __construct($options)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'size' => ['sm', 'lg', 'xl', ''],
|
||||
'type' => ['ok-only','confirm','confirm-success','confirm-warning','confirm-danger', 'custom'],
|
||||
'type' => ['ok-only', 'confirm', 'confirm-success', 'confirm-warning', 'confirm-danger', 'custom'],
|
||||
'variant' => array_merge(BootstrapGeneric::$variants, ['']),
|
||||
];
|
||||
$this->processOptions($options);
|
||||
|
@ -1066,6 +1105,7 @@ class BoostrapModal extends BootstrapGeneric {
|
|||
|
||||
private function genModal()
|
||||
{
|
||||
$this->options['modalClass'] = !empty($this->options['modalClass']) && !is_array($this->options['modalClass']) ? [$this->options['modalClass']] : $this->options['modalClass'];
|
||||
$dialog = $this->openNode('div', [
|
||||
'class' => array_merge(
|
||||
['modal-dialog', (!empty($this->options['size'])) ? "modal-{$this->options['size']}" : ''],
|
||||
|
@ -1126,7 +1166,8 @@ class BoostrapModal extends BootstrapGeneric {
|
|||
return $footer;
|
||||
}
|
||||
|
||||
private function getFooterBasedOnType() {
|
||||
private function getFooterBasedOnType()
|
||||
{
|
||||
if ($this->options['type'] == 'ok-only') {
|
||||
return $this->getFooterOkOnly();
|
||||
} else if (str_contains($this->options['type'], 'confirm')) {
|
||||
|
@ -1238,7 +1279,7 @@ class BoostrapCard extends BootstrapGeneric
|
|||
'card',
|
||||
!empty($this->options['variant']) ? "bg-{$this->options['variant']}" : '',
|
||||
!empty($this->options['variant']) ? $this->getTextClassForVariant($this->options['variant']) : '',
|
||||
h($this->options['class']),
|
||||
h(is_array($this->options['class']) ? implode(' ', $this->options['class']) : $this->options['class']),
|
||||
],
|
||||
], implode('', [$this->genHeader(), $this->genBody(), $this->genFooter()]));
|
||||
return $card;
|
||||
|
@ -1290,7 +1331,8 @@ class BoostrapCard extends BootstrapGeneric
|
|||
}
|
||||
}
|
||||
|
||||
class BoostrapSwitch extends BootstrapGeneric {
|
||||
class BoostrapSwitch extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'label' => '',
|
||||
'variant' => 'primary',
|
||||
|
@ -1301,7 +1343,8 @@ class BoostrapSwitch extends BootstrapGeneric {
|
|||
'attrs' => [],
|
||||
];
|
||||
|
||||
function __construct($options) {
|
||||
function __construct($options)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => BootstrapGeneric::$variants,
|
||||
];
|
||||
|
@ -1344,7 +1387,69 @@ class BoostrapSwitch extends BootstrapGeneric {
|
|||
}
|
||||
}
|
||||
|
||||
class BoostrapProgress extends BootstrapGeneric {
|
||||
class BoostrapNotificationBuble extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'label' => '',
|
||||
'variant' => 'warning',
|
||||
'borderVariant' => 'ligth',
|
||||
'title' => 'Notification',
|
||||
'class' => [],
|
||||
'attrs' => [],
|
||||
];
|
||||
|
||||
function __construct($options)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => BootstrapGeneric::$variants,
|
||||
];
|
||||
$this->defaultOptions['label'] = __('New notifications');
|
||||
$this->processOptions($options);
|
||||
}
|
||||
|
||||
private function processOptions($options)
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->checkOptionValidity();
|
||||
if (!empty($this->options['attrs']['style'])) {
|
||||
$this->options['attrs']['style'] += 'box-shadow: 0 0.125rem 0.25rem #00000050;';
|
||||
} else {
|
||||
$this->options['attrs']['style'] = 'box-shadow: 0 0.125rem 0.25rem #00000050;';
|
||||
}
|
||||
}
|
||||
|
||||
public function notificationBubble()
|
||||
{
|
||||
return $this->genNotificationBubble();
|
||||
}
|
||||
|
||||
private function genNotificationBubble()
|
||||
{
|
||||
$tmpId = 'tmp-' . mt_rand();
|
||||
$html = $this->genNode('span', [
|
||||
'id' => $tmpId,
|
||||
'class' => [
|
||||
'position-absolute',
|
||||
'top-0',
|
||||
'start-100',
|
||||
'translate-middle',
|
||||
'p-1',
|
||||
'border border-2 rounded-circle',
|
||||
"border-{$this->options['borderVariant']}",
|
||||
"bg-{$this->options['variant']}",
|
||||
],
|
||||
'title' => $this->options['title']
|
||||
], $this->genNode('span', [
|
||||
'class' => [
|
||||
],
|
||||
$this->options['label']
|
||||
]));
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
class BoostrapProgress extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'value' => 0,
|
||||
'total' => 100,
|
||||
|
@ -1357,7 +1462,8 @@ class BoostrapProgress extends BootstrapGeneric {
|
|||
'label' => true
|
||||
];
|
||||
|
||||
function __construct($options) {
|
||||
function __construct($options)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => BootstrapGeneric::$variants,
|
||||
];
|
||||
|
@ -1389,7 +1495,7 @@ class BoostrapProgress extends BootstrapGeneric {
|
|||
$this->options['animated'] ? 'progress-bar-animated' : '',
|
||||
],
|
||||
'role' => "progressbar",
|
||||
'aria-valuemin' => "0", 'aria-valuemax' => "100",'aria-valuenow' => $percentage,
|
||||
'aria-valuemin' => "0", 'aria-valuemax' => "100", 'aria-valuenow' => $percentage,
|
||||
'style' => "${widthStyle}",
|
||||
'title' => $this->options['title']
|
||||
], $label);
|
||||
|
@ -1404,13 +1510,15 @@ class BoostrapProgress extends BootstrapGeneric {
|
|||
}
|
||||
}
|
||||
|
||||
class BoostrapCollapse extends BootstrapGeneric {
|
||||
class BoostrapCollapse extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'text' => '',
|
||||
'title' => '',
|
||||
'open' => false,
|
||||
];
|
||||
|
||||
function __construct($options, $content, $btHelper) {
|
||||
function __construct($options, $content, $btHelper)
|
||||
{
|
||||
$this->allowedOptionValues = [];
|
||||
$this->processOptions($options);
|
||||
$this->content = $content;
|
||||
|
@ -1461,7 +1569,98 @@ class BoostrapCollapse extends BootstrapGeneric {
|
|||
}
|
||||
}
|
||||
|
||||
class BoostrapProgressTimeline extends BootstrapGeneric {
|
||||
class BoostrapAccordion extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'stayOpen' => true,
|
||||
'class' => [],
|
||||
];
|
||||
|
||||
function __construct($options, $content, $btHelper)
|
||||
{
|
||||
$this->allowedOptionValues = [];
|
||||
$this->content = $content;
|
||||
$this->btHelper = $btHelper;
|
||||
$this->processOptions($options);
|
||||
}
|
||||
|
||||
private function processOptions($options)
|
||||
{
|
||||
$this->options = array_merge($this->defaultOptions, $options);
|
||||
$this->checkOptionValidity();
|
||||
if (!is_array($this->options['class']) && !empty($this->options['class'])) {
|
||||
$this->options['class'] = [$this->options['class']];
|
||||
}
|
||||
$this->seed = 'acc-' . mt_rand();
|
||||
$this->contentSeeds = [];
|
||||
foreach ($this->content as $accordionItem) {
|
||||
$this->contentSeeds[] = mt_rand();
|
||||
}
|
||||
}
|
||||
|
||||
public function accordion()
|
||||
{
|
||||
return $this->genAccordion();
|
||||
}
|
||||
|
||||
private function genHeader($accordionItem, $i)
|
||||
{
|
||||
$html = $this->openNode('h2', [
|
||||
'class' => ['accordion-header'],
|
||||
'id' => 'head-' . $this->contentSeeds[$i]
|
||||
]);
|
||||
$content = !empty($accordionItem['header']['html']) ? $accordionItem['header']['html'] : h($accordionItem['header']['title'] ?? '- no title -');
|
||||
$buttonOptions = [
|
||||
'class' => array_merge(['accordion-button', empty($accordionItem['_open']) ? 'collapsed' : ''], $accordionItem['header']['__class'] ?? []),
|
||||
'type' => 'button',
|
||||
'data-bs-toggle' => 'collapse',
|
||||
'data-bs-target' => '#body-' . $this->contentSeeds[$i],
|
||||
'aria-expanded' => 'false',
|
||||
'aria-controls' => 'body-' . $this->contentSeeds[$i],
|
||||
];
|
||||
$html .= $this->genNode('button', $buttonOptions, $content);
|
||||
$html .= $this->closeNode(('h2'));
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genBody($accordionItem, $i)
|
||||
{
|
||||
$content = $this->genNode('div', [
|
||||
'class' => ['accordion-body']
|
||||
], $accordionItem['body']);
|
||||
$divOptions = [
|
||||
'class' => array_merge(['accordion-collapse collapse', empty($accordionItem['_open']) ? '' : 'show'], $accordionItem['body']['__class'] ?? []),
|
||||
'id' => 'body-' . $this->contentSeeds[$i],
|
||||
'aria-labelledby' => 'head-' . $this->contentSeeds[$i],
|
||||
];
|
||||
if (!empty($this->options['stayOpen'])) {
|
||||
$divOptions['data-bs-parent'] = '#' . $this->seed;
|
||||
}
|
||||
$html = $this->genNode('div', $divOptions, $content);
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function genAccordion()
|
||||
{
|
||||
$html = $this->openNode('div', [
|
||||
'class' => array_merge(['accordion'], $this->options['class']),
|
||||
'id' => $this->seed
|
||||
]);
|
||||
foreach ($this->content as $i => $accordionItem) {
|
||||
$html .= $this->openNode('div', [
|
||||
'class' => array_merge(['accordion-item'], $accordionItem['__class'] ?? [])
|
||||
]);
|
||||
$html .= $this->genHeader($accordionItem, $i);
|
||||
$html .= $this->genBody($accordionItem, $i);
|
||||
$html .= $this->closeNode('div');
|
||||
}
|
||||
$html .= $this->closeNode('div');
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
class BoostrapProgressTimeline extends BootstrapGeneric
|
||||
{
|
||||
private $defaultOptions = [
|
||||
'steps' => [],
|
||||
'selected' => 0,
|
||||
|
@ -1469,7 +1668,8 @@ class BoostrapProgressTimeline extends BootstrapGeneric {
|
|||
'variantInactive' => 'secondary',
|
||||
];
|
||||
|
||||
function __construct($options, $btHelper) {
|
||||
function __construct($options, $btHelper)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'variant' => BootstrapGeneric::$variants,
|
||||
'variantInactive' => BootstrapGeneric::$variants,
|
||||
|
@ -1496,7 +1696,7 @@ class BoostrapProgressTimeline extends BootstrapGeneric {
|
|||
!empty($step['icon']) ? h($this->btHelper->FontAwesome->getClass($step['icon'])) : '',
|
||||
$this->getTextClassForVariant($this->options['variant'])
|
||||
],
|
||||
], empty($step['icon']) ? h($i+1) : '');
|
||||
], empty($step['icon']) ? h($i + 1) : '');
|
||||
$iconContainer = $this->genNode('span', [
|
||||
'class' => [
|
||||
'd-flex', 'align-items-center', 'justify-content-center',
|
||||
|
@ -1518,7 +1718,7 @@ class BoostrapProgressTimeline extends BootstrapGeneric {
|
|||
private function getHorizontalLine($i, $nodeActive, $lineActive)
|
||||
{
|
||||
$stepCount = count($this->options['steps']);
|
||||
if ($i == $stepCount-1) {
|
||||
if ($i == $stepCount - 1) {
|
||||
return '';
|
||||
}
|
||||
$progressBar = (new BoostrapProgress([
|
||||
|
@ -1583,7 +1783,8 @@ class BootstrapListGroup extends BootstrapGeneric
|
|||
|
||||
private $bsClasses = null;
|
||||
|
||||
function __construct($options, $data, $btHelper) {
|
||||
function __construct($options, $data, $btHelper)
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->processOptions($options);
|
||||
$this->btHelper = $btHelper;
|
||||
|
@ -1685,7 +1886,8 @@ class BoostrapDropdownMenu extends BootstrapGeneric
|
|||
'submenu_classes' => [],
|
||||
];
|
||||
|
||||
function __construct($options, $btHelper) {
|
||||
function __construct($options, $btHelper)
|
||||
{
|
||||
$this->allowedOptionValues = [
|
||||
'direction' => ['start', 'end', 'up', 'down'],
|
||||
'alignment' => ['start', 'end'],
|
||||
|
@ -1714,7 +1916,7 @@ class BoostrapDropdownMenu extends BootstrapGeneric
|
|||
return $this->genDropdownWrapper($this->genDropdownToggleButton(), $this->genDropdownMenu($this->menu));
|
||||
}
|
||||
|
||||
public function genDropdownWrapper($toggle='', $menu='', $direction=null, $classes=null)
|
||||
public function genDropdownWrapper($toggle = '', $menu = '', $direction = null, $classes = null)
|
||||
{
|
||||
$classes = !is_null($classes) ? $classes : $this->options['dropdown-class'];
|
||||
$direction = !is_null($direction) ? $direction : $this->options['direction'];
|
||||
|
@ -1747,7 +1949,7 @@ class BoostrapDropdownMenu extends BootstrapGeneric
|
|||
return $this->btHelper->button($options);
|
||||
}
|
||||
|
||||
private function genDropdownMenu($entries, $alignment=null)
|
||||
private function genDropdownMenu($entries, $alignment = null)
|
||||
{
|
||||
$alignment = !is_null($alignment) ? $alignment : $this->options['alignment'];
|
||||
$html = $this->genNode('div', [
|
||||
|
@ -1775,16 +1977,32 @@ class BoostrapDropdownMenu extends BootstrapGeneric
|
|||
if (!empty($entry['html'])) {
|
||||
return $entry['html'];
|
||||
}
|
||||
$classes = [];
|
||||
$icon = '';
|
||||
if (!empty($entry['icon'])) {
|
||||
$icon = $this->btHelper->icon($entry['icon'], ['class' => 'me-2']);
|
||||
}
|
||||
$badge = '';
|
||||
if (!empty($entry['badge'])) {
|
||||
$bsBadge = new BoostrapBadge(array_merge(
|
||||
['class' => ['ms-auto']],
|
||||
$entry['badge']
|
||||
));
|
||||
$badge = $bsBadge->badge();
|
||||
}
|
||||
|
||||
if (!empty($entry['header'])) {
|
||||
return $this->genNode('h6', [
|
||||
'class' => ['dropdown-header',],
|
||||
], $icon . h($entry['text']) . $badge);
|
||||
}
|
||||
|
||||
$classes = ['dropdown-item'];
|
||||
$params = ['href' => '#'];
|
||||
$icon = '';
|
||||
if (!empty($entry['icon'])) {
|
||||
$icon = $this->btHelper->icon($entry['icon']);
|
||||
}
|
||||
|
||||
if (!empty($entry['menu'])) {
|
||||
$classes[] = 'dropdown-toggle';
|
||||
$classes[] = 'd-flex align-items-center';
|
||||
$params['data-bs-toggle'] = 'dropdown';
|
||||
$params['aria-haspopup'] = 'true';
|
||||
$params['aria-expanded'] = 'false';
|
||||
|
@ -1794,13 +2012,11 @@ class BoostrapDropdownMenu extends BootstrapGeneric
|
|||
$params['data-open-form-id'] = mt_rand();
|
||||
}
|
||||
|
||||
$label = $this->genNode('span', [
|
||||
'class' => ['ms-2',],
|
||||
], h($entry['text']));
|
||||
$content = $icon . $label;
|
||||
$label = $this->genNode('span', ['class' => 'mx-1'], h($entry['text']));
|
||||
$content = $icon . $label . $badge;
|
||||
|
||||
return $this->genNode('a', array_merge([
|
||||
'class' => $classes,
|
||||
], $params), $content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Helper;
|
||||
|
||||
use Cake\View\Helper;
|
||||
|
||||
class ValueGetterHelper extends Helper
|
||||
{
|
||||
public function get($target, $args=[])
|
||||
{
|
||||
$value = '';
|
||||
if (is_callable($target)) {
|
||||
$value = $this->eval($target, $args);
|
||||
} else {
|
||||
$value = h($target);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function eval($fun, $args=[])
|
||||
{
|
||||
return $fun($args);
|
||||
}
|
||||
}
|
|
@ -1,2 +1,2 @@
|
|||
<redoc spec-url='<?php echo $url ?>'></redoc>
|
||||
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
|
||||
<?= $this->Html->script('redoc.standalone.js') ?>
|
|
@ -6,10 +6,14 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'children' => [
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
'searchKey' => 'value',
|
||||
'allowFilering' => true
|
||||
],
|
||||
[
|
||||
'type' => 'table_action',
|
||||
]
|
||||
]
|
||||
],
|
||||
|
|
|
@ -17,7 +17,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
|
|
|
@ -20,7 +20,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
|
|
|
@ -7,7 +7,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'children' => [
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
|
|
|
@ -7,7 +7,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'children' => [
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
|
|
|
@ -7,7 +7,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'children' => [
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
|
|
|
@ -24,7 +24,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
|
|
|
@ -29,7 +29,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
|
|
|
@ -11,7 +11,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
'requirements' => $this->request->getParam('action') === 'edit'
|
||||
),
|
||||
),
|
||||
'metaTemplates' => empty($metaTemplates) ? [] : $metaTemplates,
|
||||
'submit' => array(
|
||||
'action' => $this->request->getParam('action')
|
||||
)
|
||||
|
|
|
@ -20,7 +20,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
|
@ -28,7 +28,6 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'table_action',
|
||||
'table_setting_id' => 'individual_index',
|
||||
]
|
||||
]
|
||||
],
|
||||
|
@ -73,7 +72,6 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
'title' => __('ContactDB Individuals Index'),
|
||||
'description' => __('A list of individuals known by your Cerebrate instance. This list can get populated either directly, by adding new individuals or by fetching them from trusted remote sources. Additionally, users created for the platform will always have an individual identity.'),
|
||||
'pull' => 'right',
|
||||
'actions' => [
|
||||
[
|
||||
'url' => '/individuals/view',
|
||||
|
|
|
@ -42,8 +42,8 @@ $this->userSettingsTable = TableRegistry::getTableLocator()->get('UserSettings')
|
|||
<?= __('Activity') ?>
|
||||
</h3>
|
||||
<div class="row">
|
||||
<?php foreach ($statistics as $modelName => $statistics) : ?>
|
||||
<div class="col-sm-6 col-md-5 col-l-4 col-xl-3 mb-3">
|
||||
<?php foreach ($statistics as $modelName => $statisticForModel) : ?>
|
||||
<div class="col-sm-6 col-md-5 col-lg-4 col-xl-3 mb-3">
|
||||
<?php
|
||||
$exploded = explode('.', $modelName);
|
||||
$modelForDisplay = $exploded[count($exploded) - 1];
|
||||
|
@ -57,9 +57,9 @@ $this->userSettingsTable = TableRegistry::getTableLocator()->get('UserSettings')
|
|||
);
|
||||
echo $this->element('widgets/highlight-panel', [
|
||||
'titleHtml' => $panelTitle,
|
||||
'number' => $statistics['amount'],
|
||||
'variation' => $statistics['variation'] ?? '',
|
||||
'chartData' => $statistics['timeline'] ?? []
|
||||
'number' => $statisticForModel['created']['amount'],
|
||||
'variation' => $statisticForModel['created']['variation'] ?? null,
|
||||
'timeline' => $statisticForModel ?? []
|
||||
]);
|
||||
?>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'children' => [
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
|
|
|
@ -36,7 +36,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
|
|
|
@ -6,7 +6,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'children' => [
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
echo $this->element('genericElements/Form/genericForm', [
|
||||
'data' => [
|
||||
'description' => __('Mailing list are email distribution lists containing individuals.'),
|
||||
'model' => 'MailingLists',
|
||||
'fields' => [
|
||||
[
|
||||
'field' => 'name'
|
||||
],
|
||||
[
|
||||
'field' => 'uuid',
|
||||
'label' => 'UUID',
|
||||
'type' => 'uuid'
|
||||
],
|
||||
[
|
||||
'field' => 'releasability',
|
||||
'type' => 'textarea'
|
||||
],
|
||||
[
|
||||
'field' => 'description',
|
||||
'type' => 'textarea'
|
||||
],
|
||||
[
|
||||
'field' => 'active',
|
||||
'type' => 'checkbox',
|
||||
'default' => 1
|
||||
],
|
||||
],
|
||||
'submit' => [
|
||||
'action' => $this->request->getParam('action')
|
||||
]
|
||||
]
|
||||
]);
|
||||
?>
|
||||
</div>
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
echo $this->element('genericElements/Form/genericForm', [
|
||||
'data' => [
|
||||
'title' => __('Add members to `{0}` [{1}]', h($mailingList->name), h($mailingList->id)),
|
||||
'model' => 'MailingLists',
|
||||
'fields' => [
|
||||
[
|
||||
'field' => 'individuals',
|
||||
'type' => 'dropdown',
|
||||
'multiple' => true,
|
||||
'select2' => true,
|
||||
'label' => __('Members'),
|
||||
'class' => 'select2-input',
|
||||
'options' => $dropdownData['individuals']
|
||||
],
|
||||
[
|
||||
'field' => 'chosen_emails',
|
||||
'type' => 'text',
|
||||
'templates' => ['inputContainer' => '<div class="row mb-3 d-none">{{content}}</div>'],
|
||||
],
|
||||
'<div class="alternate-emails-container panel d-none"></div>'
|
||||
],
|
||||
'submit' => [
|
||||
'action' => $this->request->getParam('action')
|
||||
],
|
||||
],
|
||||
]);
|
||||
?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
let individuals = {}
|
||||
$('#individuals-field').on('select2:select select2:unselect', function(e) {
|
||||
const selected = e.params.data;
|
||||
fetchIndividual(selected.id).then(() => {
|
||||
udpateAvailableEmails($(e.target).select2('data'));
|
||||
})
|
||||
});
|
||||
|
||||
function udpateAvailableEmails(selected) {
|
||||
const $container = $('.alternate-emails-container')
|
||||
$container.empty()
|
||||
$container.toggleClass('d-none', selected.length == 0)
|
||||
selected.forEach(selectData => {
|
||||
const individual = individuals[selectData.id]
|
||||
let formContainers = [genForContainer(`primary-${individual.id}`, individual.email, '<?= __('primary email') ?>', true, true)]
|
||||
if (individual.alternate_emails !== undefined) {
|
||||
individual.alternate_emails.forEach(alternateEmail => {
|
||||
formContainers.push(
|
||||
genForContainer(alternateEmail.id, alternateEmail.value, `${alternateEmail.meta_template_field.meta_template.namespace} :: ${alternateEmail.field}`, false)
|
||||
)
|
||||
})
|
||||
}
|
||||
const $individualFullName = $('<div/>').addClass('fw-light fs-5 mt-2').text(individual.full_name)
|
||||
const $individualContainer = $('<div/>').addClass('individual-container').data('individualid', individual.id)
|
||||
.append($individualFullName).append(formContainers)
|
||||
$container.append($individualContainer)
|
||||
registerChangeListener()
|
||||
injectSelectedEmailsIntoForm()
|
||||
});
|
||||
}
|
||||
|
||||
function genForContainer(id, email, email_source, is_primary = true, checked = false) {
|
||||
const $formContainer = $('<div/>').addClass('form-check ms-2')
|
||||
$formContainer.append(
|
||||
$('<input/>').addClass('form-check-input').attr('type', 'checkbox').attr('id', `individual-${id}`)
|
||||
.attr('value', is_primary ? 'primary' : id).prop('checked', checked),
|
||||
$('<label/>').addClass('form-check-label').attr('for', `individual-${id}`).append(
|
||||
$('<span/>').text(email),
|
||||
$('<span/>').addClass('fw-light fs-8 align-middle ms-2').text(`${email_source}`)
|
||||
)
|
||||
)
|
||||
return $formContainer
|
||||
}
|
||||
|
||||
function registerChangeListener() {
|
||||
$('.alternate-emails-container .individual-container input')
|
||||
.off('change.udpate')
|
||||
.on('change.udpate', injectSelectedEmailsIntoForm)
|
||||
}
|
||||
|
||||
function injectSelectedEmailsIntoForm() {
|
||||
const selectedEmails = getSelectedEmails()
|
||||
$('#chosen_emails-field').val(JSON.stringify(selectedEmails))
|
||||
}
|
||||
|
||||
function getSelectedEmails() {
|
||||
selectedEmails = {}
|
||||
$('.alternate-emails-container .individual-container').each(function() {
|
||||
const $individualContainer = $(this)
|
||||
const individualId = $individualContainer.data('individualid')
|
||||
selectedEmails[individualId] = []
|
||||
const $inputs = $individualContainer.find('input:checked').each(function() {
|
||||
selectedEmails[individualId].push($(this).val())
|
||||
})
|
||||
})
|
||||
return selectedEmails
|
||||
}
|
||||
|
||||
function fetchIndividual(id) {
|
||||
const urlGet = `/individuals/view/${id}?full=1`
|
||||
const options = {
|
||||
statusNode: $('.alternate-emails-container')
|
||||
}
|
||||
if (individuals[id] !== undefined) {
|
||||
return Promise.resolve(individuals[id])
|
||||
}
|
||||
return AJAXApi.quickFetchJSON(urlGet, options)
|
||||
.then(individual => {
|
||||
individuals[individual.id] = individual
|
||||
})
|
||||
.catch((e) => {
|
||||
UI.toast({
|
||||
variant: 'danger',
|
||||
text: '<?= __('Could not fetch individual') ?>'
|
||||
})
|
||||
})
|
||||
}
|
||||
})()
|
||||
</script>
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
echo $this->element('genericElements/IndexTable/index_table', [
|
||||
'data' => [
|
||||
'data' => $data,
|
||||
'top_bar' => [
|
||||
'children' => [
|
||||
[
|
||||
'type' => 'simple',
|
||||
'children' => [
|
||||
'data' => [
|
||||
'type' => 'simple',
|
||||
'text' => __('Add mailing list'),
|
||||
'popover_url' => '/MailingLists/add'
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
],
|
||||
[
|
||||
'type' => 'table_action',
|
||||
'table_setting_id' => 'mailinglist_index',
|
||||
]
|
||||
]
|
||||
],
|
||||
'fields' => [
|
||||
[
|
||||
'name' => '#',
|
||||
'sort' => 'id',
|
||||
'class' => 'short',
|
||||
'data_path' => 'id',
|
||||
],
|
||||
[
|
||||
'name' => __('Name'),
|
||||
'sort' => 'name',
|
||||
'data_path' => 'name',
|
||||
],
|
||||
[
|
||||
'name' => __('Owner'),
|
||||
'data_path' => 'user_id',
|
||||
'element' => 'user'
|
||||
],
|
||||
[
|
||||
'name' => __('UUID'),
|
||||
'sort' => 'uuid',
|
||||
'data_path' => 'uuid',
|
||||
],
|
||||
[
|
||||
'name' => __('Members'),
|
||||
'data_path' => 'individuals',
|
||||
'element' => 'count_summary',
|
||||
],
|
||||
[
|
||||
'name' => __('Intended recipients'),
|
||||
'data_path' => 'recipients',
|
||||
],
|
||||
[
|
||||
'name' => __('Description'),
|
||||
'data_path' => 'description',
|
||||
],
|
||||
[
|
||||
'name' => __('Active'),
|
||||
'data_path' => 'active',
|
||||
'sort' => 'active',
|
||||
'element' => 'boolean',
|
||||
],
|
||||
[
|
||||
'name' => __('Deleted'),
|
||||
'data_path' => 'deleted',
|
||||
'sort' => 'deleted',
|
||||
'element' => 'boolean',
|
||||
],
|
||||
],
|
||||
'title' => __('Mailing Lists Index'),
|
||||
'description' => __('Mailing list are email distribution lists containing individuals.'),
|
||||
'actions' => [
|
||||
[
|
||||
'url' => '/mailingLists/view',
|
||||
'url_params_data_paths' => ['id'],
|
||||
'icon' => 'eye'
|
||||
],
|
||||
[
|
||||
'open_modal' => '/mailingLists/edit/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'edit'
|
||||
],
|
||||
[
|
||||
'open_modal' => '/mailingLists/delete/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'trash'
|
||||
],
|
||||
]
|
||||
]
|
||||
]);
|
||||
?>
|
|
@ -0,0 +1,142 @@
|
|||
<?php
|
||||
echo $this->element('genericElements/IndexTable/index_table', [
|
||||
'data' => [
|
||||
'data' => $individuals,
|
||||
'top_bar' => [
|
||||
'children' => [
|
||||
[
|
||||
'type' => 'multi_select_actions',
|
||||
'children' => [
|
||||
[
|
||||
'text' => __('Remove members'),
|
||||
'variant' => 'danger',
|
||||
'onclick' => 'removeMembers',
|
||||
]
|
||||
],
|
||||
'data' => [
|
||||
'id' => [
|
||||
'value_path' => 'id'
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'simple',
|
||||
'children' => [
|
||||
'data' => [
|
||||
'type' => 'simple',
|
||||
'text' => __('Add member'),
|
||||
'popover_url' => '/mailingLists/addIndividual/' . h($mailing_list_id),
|
||||
'reload_url' => '/mailingLists/listIndividuals/' . h($mailing_list_id)
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
'additionalUrlParams' => h($mailing_list_id)
|
||||
],
|
||||
]
|
||||
],
|
||||
'fields' => [
|
||||
[
|
||||
'name' => '#',
|
||||
'sort' => 'id',
|
||||
'data_path' => 'id',
|
||||
'url' => '/individuals/view/{{0}}',
|
||||
'url_vars' => ['id'],
|
||||
],
|
||||
[
|
||||
'name' => __('First name'),
|
||||
'data_path' => 'first_name',
|
||||
'url' => '/individuals/view/{{0}}',
|
||||
'url_vars' => ['id'],
|
||||
],
|
||||
[
|
||||
'name' => __('Last name'),
|
||||
'data_path' => 'last_name',
|
||||
'url' => '/individuals/view/{{0}}',
|
||||
'url_vars' => ['id'],
|
||||
],
|
||||
[
|
||||
'name' => __('Registered Email'),
|
||||
'data_path' => 'mailinglist_emails',
|
||||
'element' => 'list',
|
||||
],
|
||||
[
|
||||
'name' => __('UUID'),
|
||||
'sort' => 'uuid',
|
||||
'data_path' => 'uuid',
|
||||
]
|
||||
],
|
||||
'actions' => [
|
||||
[
|
||||
'open_modal' => '/mailingLists/removeIndividual/' . h($mailing_list_id) . '/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'reload_url' => '/mailingLists/listIndividuals/' . h($mailing_list_id),
|
||||
'icon' => 'trash'
|
||||
],
|
||||
]
|
||||
]
|
||||
]);
|
||||
?>
|
||||
|
||||
<script>
|
||||
function removeMembers(idList, selectedData, $table) {
|
||||
const successCallback = function([data, modalObject]) {
|
||||
UI.reload('/mailingLists/listIndividuals/<?= h($mailing_list_id) ?>', UI.getContainerForTable($table), $table)
|
||||
}
|
||||
const failCallback = ([data, modalObject]) => {
|
||||
const tableData = selectedData.map(row => {
|
||||
entryInError = data.filter(error => error.data.id == row.id)[0]
|
||||
$faIcon = $('<i class="fa"></i>').addClass(entryInError.success ? 'fa-check text-success' : 'fa-times text-danger')
|
||||
return [row.id, row.first_name, row.last_name, row.email, entryInError.message, JSON.stringify(entryInError.errors), $faIcon]
|
||||
});
|
||||
handleMessageTable(
|
||||
modalObject.$modal,
|
||||
['<?= __('ID') ?>', '<?= __('First name') ?>', '<?= __('Last name') ?>', '<?= __('email') ?>', '<?= __('Message') ?>', '<?= __('Error') ?>', '<?= __('State') ?>'],
|
||||
tableData
|
||||
)
|
||||
const $footer = $(modalObject.ajaxApi.statusNode).parent()
|
||||
modalObject.ajaxApi.statusNode.remove()
|
||||
const $cancelButton = $footer.find('button[data-bs-dismiss="modal"]')
|
||||
$cancelButton.text('<?= __('OK') ?>').removeClass('btn-secondary').addClass('btn-primary')
|
||||
}
|
||||
UI.submissionModal('/mailingLists/removeIndividual/<?= h($mailing_list_id) ?>', successCallback, failCallback).then(([modalObject, ajaxApi]) => {
|
||||
const $idsInput = modalObject.$modal.find('form').find('input#ids-field')
|
||||
$idsInput.val(JSON.stringify(idList))
|
||||
const tableData = selectedData.map(row => {
|
||||
return [row.id, row.first_name, row.last_name, row.email]
|
||||
});
|
||||
handleMessageTable(
|
||||
modalObject.$modal,
|
||||
['<?= __('ID') ?>', '<?= __('First name') ?>', '<?= __('Last name') ?>', '<?= __('email') ?>'],
|
||||
tableData
|
||||
)
|
||||
})
|
||||
|
||||
function constructMessageTable(header, data) {
|
||||
return HtmlHelper.table(
|
||||
header,
|
||||
data, {
|
||||
small: true,
|
||||
borderless: true,
|
||||
tableClass: ['message-table', 'mt-4 mb-0'],
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function handleMessageTable($modal, header, data) {
|
||||
const $modalBody = $modal.find('.modal-body')
|
||||
const $messageTable = $modalBody.find('table.message-table')
|
||||
const messageTableHTML = constructMessageTable(header, data)[0].outerHTML
|
||||
if ($messageTable.length) {
|
||||
$messageTable.html(messageTableHTML)
|
||||
} else {
|
||||
$modalBody.append(messageTableHTML)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
echo $this->element(
|
||||
'/genericElements/SingleViews/single_view',
|
||||
[
|
||||
'data' => $entity,
|
||||
'fields' => [
|
||||
[
|
||||
'key' => __('ID'),
|
||||
'path' => 'id'
|
||||
],
|
||||
[
|
||||
'key' => __('UUID'),
|
||||
'path' => 'uuid'
|
||||
],
|
||||
[
|
||||
'key' => __('Name'),
|
||||
'path' => 'name'
|
||||
],
|
||||
[
|
||||
'key' => __('Owner'),
|
||||
'path' => 'user_id',
|
||||
'url' => '/users/view/{{0}}',
|
||||
'url_vars' => 'user_id'
|
||||
],
|
||||
[
|
||||
'key' => __('Releasability'),
|
||||
'path' => 'releasability'
|
||||
],
|
||||
[
|
||||
'key' => __('Description'),
|
||||
'path' => 'description'
|
||||
],
|
||||
[
|
||||
'key' => __('Active'),
|
||||
'path' => 'active',
|
||||
'type' => 'boolean'
|
||||
],
|
||||
[
|
||||
'key' => __('Deleted'),
|
||||
'path' => 'deleted',
|
||||
'type' => 'boolean'
|
||||
]
|
||||
],
|
||||
'children' => [
|
||||
[
|
||||
'url' => '/mailingLists/listIndividuals/{{0}}',
|
||||
'url_params' => ['id'],
|
||||
'title' => __('Individuals'),
|
||||
'collapsed' => 'show',
|
||||
]
|
||||
]
|
||||
]
|
||||
);
|
|
@ -6,7 +6,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'children' => [
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
|
@ -34,7 +34,12 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'name' => __('Validation regex'),
|
||||
'sort' => 'regex',
|
||||
'data_path' => 'regex'
|
||||
]
|
||||
],
|
||||
[
|
||||
'name' => __('Field Usage'),
|
||||
'sort' => 'counter',
|
||||
'data_path' => 'counter',
|
||||
],
|
||||
],
|
||||
'title' => __('Meta Template Fields'),
|
||||
'description' => __('The various fields that the given template contans. When a meta template is enabled, the fields are automatically appended to the appropriate object.'),
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
|
||||
<?php
|
||||
use Cake\Utility\Inflector;
|
||||
use Cake\Routing\Router;
|
||||
|
||||
$urlNewestMetaTemplate = Router::url([
|
||||
'controller' => 'metaTemplates',
|
||||
'action' => 'view',
|
||||
$newestMetaTemplate->id
|
||||
]);
|
||||
|
||||
$bodyHtml = '';
|
||||
$bodyHtml .= sprintf('<div><span>%s: </span><span class="fw-bold">%s</span></div>', __('Current version'), h($metaTemplate->version));
|
||||
$bodyHtml .= sprintf('<div><span>%s: </span><a href="%s" target="_blank" class="fw-bold">%s</a></div>', __('Newest version'), $urlNewestMetaTemplate, h($newestMetaTemplate->version));
|
||||
$bodyHtml .= sprintf('<h4 class="my-2">%s</h4>', __('Entities with meta-fields to be updated:'));
|
||||
|
||||
$bodyHtml .= '<ul>';
|
||||
foreach ($entities as $entity) {
|
||||
$url = Router::url([
|
||||
'controller' => Inflector::pluralize($metaTemplate->scope),
|
||||
'action' => 'view',
|
||||
$entity->id
|
||||
]);
|
||||
$bodyHtml .= sprintf(
|
||||
'<li><a href="%s" target="_blank">%s</a> <span class="fw-light">%s<span></li>',
|
||||
$url,
|
||||
__('{0}::{1}', h(Inflector::humanize($metaTemplate->scope)), $entity->id),
|
||||
__('has {0} meta-fields to update', count($entity->meta_fields))
|
||||
);
|
||||
}
|
||||
if ($amountOfEntitiesToUpdate > 10) {
|
||||
$bodyHtml .= sprintf('<li class="list-inline-item fw-light fs-7">%s</li>', __('{0} more entities', h(10 - $amountOfEntitiesToUpdate)));
|
||||
}
|
||||
$bodyHtml .= '</ul>';
|
||||
|
||||
echo $this->Bootstrap->modal([
|
||||
'titleHtml' => __('{0} has a new meta-template and meta-fields to be updated', sprintf('<i class="me-1">%s</i>', h($metaTemplate->name))),
|
||||
'bodyHtml' => $bodyHtml,
|
||||
'size' => 'lg',
|
||||
'type' => 'ok-only',
|
||||
]);
|
||||
?>
|
|
@ -1,5 +1,34 @@
|
|||
<?php
|
||||
use Cake\Utility\Hash;
|
||||
|
||||
if (!empty($updateableTemplates['new'])) {
|
||||
$alertHtml = sprintf(
|
||||
'<strong>%s</strong> %s',
|
||||
__('New meta-templates available!'),
|
||||
__n('There is one new template on disk that can be loaded in the database', 'There are {0} new templates on disk that can be loaded in the database:', count($updateableTemplates['new']), count($updateableTemplates['new']))
|
||||
);
|
||||
$alertList = [];
|
||||
$alertList = Hash::extract($updateableTemplates['new'], '{s}.template');
|
||||
$alertList = array_map(function($entry) {
|
||||
return sprintf('%s:%s %s',
|
||||
h($entry['namespace']),
|
||||
h($entry['name']),
|
||||
$this->Bootstrap->button([
|
||||
'variant' => 'link',
|
||||
'size' => 'sm',
|
||||
'icon' => 'download',
|
||||
'title' => __('Create this template'),
|
||||
'params' => [
|
||||
'onclick' => "UI.submissionModalForIndex('/metaTemplates/createNewTemplate/{$entry['uuid']}', '/meta-templates')"
|
||||
]
|
||||
])
|
||||
);
|
||||
}, $alertList);
|
||||
$alertHtml .= $this->Html->nestedList($alertList);
|
||||
}
|
||||
|
||||
echo $this->element('genericElements/IndexTable/index_table', [
|
||||
'notice' => !empty($alertHtml) ? ['html' => $alertHtml, 'variant' => 'warning',] : false,
|
||||
'data' => [
|
||||
'data' => $data,
|
||||
'top_bar' => [
|
||||
|
@ -10,7 +39,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
|
@ -129,21 +158,60 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'sort' => 'namespace',
|
||||
'data_path' => 'namespace',
|
||||
],
|
||||
[
|
||||
'name' => __('Version'),
|
||||
'sort' => 'version',
|
||||
'data_path' => 'version',
|
||||
],
|
||||
[
|
||||
'name' => __('UUID'),
|
||||
'sort' => 'uuid',
|
||||
'data_path' => 'uuid'
|
||||
]
|
||||
],
|
||||
],
|
||||
'title' => __('Meta Field Templates'),
|
||||
'description' => __('The various templates used to enrich certain objects by a set of standardised fields.'),
|
||||
'pull' => 'right',
|
||||
'actions' => [
|
||||
[
|
||||
'url' => '/metaTemplates/view',
|
||||
'url_params_data_paths' => ['id'],
|
||||
'icon' => 'eye'
|
||||
],
|
||||
[
|
||||
'open_modal' => '/metaTemplates/update/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'title' => __('Update Meta-Template'),
|
||||
'icon' => 'download',
|
||||
'complex_requirement' => [
|
||||
'function' => function ($row, $options) {
|
||||
return empty($row['updateStatus']['up-to-date']) && empty($row['updateStatus']['to-existing']);
|
||||
}
|
||||
]
|
||||
],
|
||||
[
|
||||
'open_modal' => '/metaTemplates/getMetaFieldsToUpdate/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'title' => __('Get meta-fields that should be moved to the newest version of this meta-template'),
|
||||
'icon' => 'exclamation-triangle',
|
||||
'variant' => 'warning',
|
||||
'complex_requirement' => [
|
||||
'function' => function ($row, $options) {
|
||||
return !empty($row['updateStatus']['to-existing']) && empty($row['updateStatus']['can-be-removed']);
|
||||
}
|
||||
]
|
||||
],
|
||||
[
|
||||
'open_modal' => '/metaTemplates/delete/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'title' => __('Get meta-fields that should be moved to the newest version of this meta-template'),
|
||||
'icon' => 'trash',
|
||||
'variant' => 'success',
|
||||
'complex_requirement' => [
|
||||
'function' => function ($row, $options) {
|
||||
return !empty($row['updateStatus']['to-existing']) && !empty($row['updateStatus']['can-be-removed']);
|
||||
}
|
||||
]
|
||||
],
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
use Cake\Utility\Inflector;
|
||||
use Cake\Routing\Router;
|
||||
?>
|
||||
<h3><?= h($oldMetaTemplate->name) ?></h3>
|
||||
<div class="container-fluid">
|
||||
<div class="row gx-2">
|
||||
<div class="col">
|
||||
<div class="panel">
|
||||
<h4 class="d-flex justify-content-between align-items-center">
|
||||
<?php
|
||||
$url = Router::url([
|
||||
'action' => 'view',
|
||||
$oldMetaTemplate->id
|
||||
]);
|
||||
?>
|
||||
<a href="<?= $url ?>" class="text-decoration-none" target="__blank"><?= __('Version {0}', h($oldMetaTemplate->version)) ?></a>
|
||||
<?=
|
||||
$this->Bootstrap->badge([
|
||||
'text' => __('Data to be migrated over'),
|
||||
'variant' => 'danger',
|
||||
'class' => 'fs-7'
|
||||
])
|
||||
?>
|
||||
</h4>
|
||||
<div>
|
||||
<?=
|
||||
$this->element('MetaTemplates/migrationToNewVersionForm', [
|
||||
'metaTemplate' => $oldMetaTemplate,
|
||||
'entity' => $entity,
|
||||
])
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col pt-4 d-flex justify-content-center" style="max-width: 32px;">
|
||||
<?= $this->Bootstrap->icon('arrow-alt-circle-right') ?>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="panel">
|
||||
<h4 class="d-flex justify-content-between align-items-center">
|
||||
<?php
|
||||
$url = Router::url([
|
||||
'action' => 'view',
|
||||
$newMetaTemplate->id
|
||||
]);
|
||||
?>
|
||||
<a href="<?= $url ?>" class="text-decoration-none" target="__blank"><?= __('Version {0}', h($newMetaTemplate->version)) ?></a>
|
||||
<?=
|
||||
$this->Bootstrap->badge([
|
||||
'text' => __('Data to be saved'),
|
||||
'variant' => 'success',
|
||||
'class' => 'fs-7'
|
||||
])
|
||||
?>
|
||||
</h4>
|
||||
<div class="to-save-container">
|
||||
<?=
|
||||
$this->element('MetaTemplates/migrationToNewVersionForm', [
|
||||
'metaTemplate' => $newMetaTemplate,
|
||||
'entity' => $entity,
|
||||
])
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-row-reverse">
|
||||
<?=
|
||||
$this->Bootstrap->button([
|
||||
'text' => __('Update to version {0}', h($newMetaTemplate->version)),
|
||||
'variant' => 'success',
|
||||
'params' => [
|
||||
'onclick' => 'submitMigration()'
|
||||
]
|
||||
])
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
echo $this->Html->scriptBlock(sprintf(
|
||||
'var csrfToken = %s;',
|
||||
json_encode($this->request->getAttribute('csrfToken'))
|
||||
));
|
||||
?>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
const movedMetaTemplateFields = <?= json_encode($movedMetaTemplateFields) ?>;
|
||||
const oldMetaTemplateID = <?= h($oldMetaTemplate->id) ?>;
|
||||
movedMetaTemplateFields.forEach(metaTemplateId => {
|
||||
let validInputPath = `MetaTemplates.${oldMetaTemplateID}.meta_template_fields.${movedMetaTemplateFields}`
|
||||
const $inputs = $(`input[field^="${validInputPath}"]`)
|
||||
$inputs.addClass('is-valid');
|
||||
});
|
||||
})
|
||||
|
||||
function submitMigration() {
|
||||
const $form = $('.to-save-container form')
|
||||
console.log($form.attr('action'));
|
||||
AJAXApi.quickPostForm($form[0]).then((postResult) => {
|
||||
if (postResult.additionalData.redirect.url !== undefined) {
|
||||
window.location = postResult.additionalData.redirect.url
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
use App\Model\Table\MetaTemplatesTable;
|
||||
|
||||
$bodyHtml = '';
|
||||
$modalType = 'confirm';
|
||||
$modalSize = 'lg';
|
||||
if ($updateStatus['up-to-date']) {
|
||||
$bodyHtml .= $this->Bootstrap->alert([
|
||||
'variant' => 'success',
|
||||
'text' => __('This meta-template is already up-to-date!'),
|
||||
'dismissible' => false,
|
||||
]);
|
||||
$modalType = 'ok-only';
|
||||
} else {
|
||||
if ($updateStatus['automatically-updateable']) {
|
||||
$bodyHtml .= $this->Bootstrap->alert([
|
||||
'variant' => 'success',
|
||||
'html' => __('This meta-template can be updated to version {0} (current: {1}).', sprintf('<strong>%s</strong>', h($templateOnDisk['version'])), h($metaTemplate->version)),
|
||||
'dismissible' => false,
|
||||
]);
|
||||
$form = $this->element('genericElements/Form/genericForm', [
|
||||
'entity' => null,
|
||||
'ajax' => false,
|
||||
'raw' => true,
|
||||
'data' => [
|
||||
'model' => 'MetaTemplate',
|
||||
'fields' => [
|
||||
[
|
||||
'field' => 'update_strategy',
|
||||
'type' => 'checkbox',
|
||||
'value' => MetaTemplatesTable::UPDATE_STRATEGY_CREATE_NEW,
|
||||
'checked' => true,
|
||||
]
|
||||
],
|
||||
'submit' => [
|
||||
'action' => $this->request->getParam('action')
|
||||
],
|
||||
]
|
||||
]);
|
||||
$bodyHtml .= sprintf('<div class="d-none">%s</div>', $form);
|
||||
} else {
|
||||
$modalSize = 'xl';
|
||||
$bodyHtml .= $this->Bootstrap->alert([
|
||||
'variant' => 'warning',
|
||||
'text' => __('Updating to version {0} cannot be done automatically as it introduces some conflicts.', h($templateOnDisk['version'])),
|
||||
'dismissible' => false,
|
||||
]);
|
||||
$conflictTable = $this->element('MetaTemplates/conflictTable', [
|
||||
'templateStatus' => $templateStatus,
|
||||
'metaTemplate' => $metaTemplate,
|
||||
'templateOnDisk' => $templateOnDisk,
|
||||
]);
|
||||
$bodyHtml .= $this->Bootstrap->collapse([
|
||||
'title' => __('View conflicts'),
|
||||
'open' => false
|
||||
], $conflictTable);
|
||||
$bodyHtml .= $this->element('MetaTemplates/conflictResolution', [
|
||||
'templateStatus' => $templateStatus,
|
||||
'metaTemplate' => $metaTemplate,
|
||||
'templateOnDisk' => $templateOnDisk,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
echo $this->Bootstrap->modal([
|
||||
'title' => __('Update Meta Templates #{0} ?', h($metaTemplate->id)),
|
||||
'bodyHtml' => $bodyHtml,
|
||||
'size' => $modalSize,
|
||||
'type' => $modalType,
|
||||
'confirmText' => __('Update meta-templates'),
|
||||
]);
|
||||
?>
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
use Cake\Routing\Router;
|
||||
|
||||
$bodyHtml = '';
|
||||
$modalType = 'confirm';
|
||||
$modalSize = 'lg';
|
||||
|
||||
$tableHtml = '<table class="table"><thead><tr>';
|
||||
$tableHtml .= sprintf('<th class="text-nowrap">%s</th>', __('ID'));
|
||||
$tableHtml .= sprintf('<th class="text-nowrap">%s</th>', __('Template'));
|
||||
$tableHtml .= sprintf('<th class="text-nowrap">%s</th>', __('Version'));
|
||||
$tableHtml .= sprintf('<th class="text-nowrap">%s</th>', __('New Template'));
|
||||
$tableHtml .= sprintf('<th class="text-nowrap">%s</th>', __('Update available'));
|
||||
$tableHtml .= sprintf('<th class="text-nowrap">%s</th>', __('Has Conflicts'));
|
||||
$tableHtml .= sprintf('<th class="text-nowrap">%s</th>', __('Will be updated'));
|
||||
$tableHtml .= '</tr></thead><tbody>';
|
||||
$numberOfUpdates = 0;
|
||||
$numberOfSkippedUpdates = 0;
|
||||
foreach ($templatesUpdateStatus as $uuid => $status) {
|
||||
$tableHtml .= '<tr>';
|
||||
if (!empty($status['new'])) {
|
||||
$tableHtml .= sprintf('<td>%s</td>', __('N/A'));
|
||||
} else {
|
||||
$tableHtml .= sprintf('<td><a href="%s">%s</a></td>',
|
||||
Router::url(['controller' => 'MetaTemplates', 'action' => 'view', 'plugin' => null, h($status['existing_template']->id)]),
|
||||
h($status['existing_template']->id)
|
||||
);
|
||||
}
|
||||
if (!empty($status['new'])) {
|
||||
$tableHtml .= sprintf('<td>%s</td>', h($uuid));
|
||||
} else {
|
||||
$tableHtml .= sprintf('<td><a href="%s">%s</a></td>',
|
||||
Router::url(['controller' => 'MetaTemplates', 'action' => 'view', 'plugin' => null, h($status['existing_template']->id)]),
|
||||
h($status['existing_template']->name)
|
||||
);
|
||||
}
|
||||
if (!empty($status['new'])) {
|
||||
$tableHtml .= sprintf('<td>%s</td>', __('N/A'));
|
||||
} else {
|
||||
$tableHtml .= sprintf('<td>%s %s %s</td>',
|
||||
h($status['current_version']),
|
||||
$this->Bootstrap->icon('arrow-right', ['class' => 'fs-8']),
|
||||
h($status['next_version'])
|
||||
);
|
||||
}
|
||||
if (!empty($status['new'])) {
|
||||
$numberOfUpdates += 1;
|
||||
$tableHtml .= sprintf('<td>%s</td>', $this->Bootstrap->icon('check'));
|
||||
} else {
|
||||
$tableHtml .= sprintf('<td>%s</td>', $this->Bootstrap->icon('times'));
|
||||
}
|
||||
if (!empty($status['new'])) {
|
||||
$tableHtml .= sprintf('<td>%s</td>', __('N/A'));
|
||||
} else {
|
||||
$tableHtml .= sprintf('<td>%s</td>', empty($status['up-to-date']) ? $this->Bootstrap->icon('check') : $this->Bootstrap->icon('times'));
|
||||
}
|
||||
if (!empty($status['new'])) {
|
||||
$tableHtml .= sprintf('<td>%s</td>', __('N/A'));
|
||||
} else {
|
||||
$tableHtml .= sprintf('<td>%s</td>', !empty($status['conflicts']) ? $this->Bootstrap->icon('check') : $this->Bootstrap->icon('times'));
|
||||
}
|
||||
if (!empty($status['new'])) {
|
||||
$tableHtml .= sprintf('<td>%s</td>', $this->Bootstrap->icon('check', ['class' => 'text-success']));
|
||||
} else {
|
||||
// Depends on the strategy used by the update_all function. Right now, every update create a brand new template
|
||||
// leaving existing data untouched. So regardless of the conflict, the new template will be created
|
||||
if (!empty($status['new']) || empty($status['up-to-date'])) {
|
||||
$numberOfUpdates += 1;
|
||||
$tableHtml .= sprintf('<td>%s</td>', $this->Bootstrap->icon('check', ['class' => 'text-success']));
|
||||
} else {
|
||||
$numberOfSkippedUpdates += 1;
|
||||
$tableHtml .= sprintf('<td>%s</td>', $this->Bootstrap->icon('times', ['class' => 'text-danger']));
|
||||
}
|
||||
}
|
||||
$tableHtml .= '</tr>';
|
||||
}
|
||||
$tableHtml .= '</tbody></table>';
|
||||
|
||||
if (empty($numberOfSkippedUpdates) && empty($numberOfUpdates)) {
|
||||
$bodyHtml .= $this->Bootstrap->alert([
|
||||
'variant' => 'success',
|
||||
'text' => __('All meta-templates are already up-to-date!'),
|
||||
'dismissible' => false,
|
||||
]);
|
||||
$modalType = 'ok-only';
|
||||
} elseif ($numberOfSkippedUpdates == 0) {
|
||||
$bodyHtml .= $this->Bootstrap->alert([
|
||||
'variant' => 'success',
|
||||
'text' => __('All {0} meta-templates can be updated', $numberOfUpdates),
|
||||
'dismissible' => false,
|
||||
]);
|
||||
} else {
|
||||
$modalSize = 'xl';
|
||||
$alertHtml = '';
|
||||
if (!empty($numberOfUpdates)) {
|
||||
$alertHtml .= sprintf('<div>%s</div>', __('{0} meta-templates can be updated.', sprintf('<strong>%s</strong>', $numberOfUpdates)));
|
||||
}
|
||||
if (!empty($numberOfSkippedUpdates)) {
|
||||
$alertHtml .= sprintf('<div>%s</div>', __('{0} meta-templates will be skipped.', sprintf('<strong>%s</strong>', $numberOfSkippedUpdates)));
|
||||
$alertHtml .= sprintf('<div>%s</div>', __('You can still choose the update strategy when updating each conflicting template manually.'));
|
||||
}
|
||||
$bodyHtml .= $this->Bootstrap->alert([
|
||||
'variant' => 'warning',
|
||||
'html' => $alertHtml,
|
||||
'dismissible' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
$bodyHtml .= $tableHtml;
|
||||
|
||||
echo $this->Bootstrap->modal([
|
||||
'title' => h($title),
|
||||
'bodyHtml' => $bodyHtml,
|
||||
'size' => $modalSize,
|
||||
'type' => $modalType,
|
||||
'confirmText' => __('Update meta-templates'),
|
||||
'confirmFunction' => 'updateMetaTemplate',
|
||||
]);
|
||||
?>
|
|
@ -43,7 +43,8 @@ echo $this->element(
|
|||
[
|
||||
'url' => '/MetaTemplateFields/index?meta_template_id={{0}}',
|
||||
'url_params' => ['id'],
|
||||
'title' => __('Fields')
|
||||
'title' => __('Fields'),
|
||||
'collapsed' => 'show',
|
||||
]
|
||||
]
|
||||
]
|
||||
|
|
|
@ -7,7 +7,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'children' => [
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
|
|
|
@ -18,7 +18,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
'field' => 'type'
|
||||
)
|
||||
),
|
||||
'metaTemplates' => empty($metaTemplates) ? [] : $metaTemplates,
|
||||
'submit' => array(
|
||||
'action' => $this->request->getParam('action')
|
||||
)
|
||||
|
|
|
@ -44,6 +44,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'name' => __('Name'),
|
||||
'class' => 'short',
|
||||
'data_path' => 'name',
|
||||
'sort' => 'name',
|
||||
],
|
||||
[
|
||||
'name' => __('UUID'),
|
||||
|
@ -67,14 +68,17 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
[
|
||||
'name' => __('Nationality'),
|
||||
'data_path' => 'nationality',
|
||||
'sort' => 'nationality',
|
||||
],
|
||||
[
|
||||
'name' => __('Sector'),
|
||||
'data_path' => 'sector',
|
||||
'sort' => 'sector',
|
||||
],
|
||||
[
|
||||
'name' => __('Type'),
|
||||
'data_path' => 'type',
|
||||
'sort' => 'type',
|
||||
],
|
||||
[
|
||||
'name' => __('Tags'),
|
||||
|
@ -84,7 +88,6 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
'title' => __('ContactDB Organisation Index'),
|
||||
'description' => __('A list of organisations known by your Cerebrate instance. This list can get populated either directly, by adding new organisations or by fetching them from trusted remote sources.'),
|
||||
'pull' => 'right',
|
||||
'actions' => [
|
||||
[
|
||||
'url' => '/organisations/view',
|
||||
|
|
|
@ -48,8 +48,7 @@ echo $this->element(
|
|||
'scope' => 'organisations'
|
||||
]
|
||||
],
|
||||
'metaTemplates' => empty($metaFields) ? [] : $metaFields,
|
||||
'combinedFieldsView' => true,
|
||||
'combinedFieldsView' => false,
|
||||
'children' => []
|
||||
]
|
||||
);
|
||||
|
|
|
@ -29,7 +29,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
|
|
|
@ -11,7 +11,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value',
|
||||
|
|
|
@ -17,7 +17,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
|
|
|
@ -17,7 +17,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
|
|
|
@ -18,7 +18,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
|
|
|
@ -22,7 +22,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
<?php
|
||||
use Cake\Core\Configure;
|
||||
$passwordRequired = false;
|
||||
$showPasswordField = false;
|
||||
if ($this->request->getParam('action') === 'add') {
|
||||
$dropdownData['individual'] = ['new' => __('New individual')] + $dropdownData['individual'];
|
||||
if (!Configure::check('password_auth.enabled') || Configure::read('password_auth.enabled')) {
|
||||
$passwordRequired = 'required';
|
||||
}
|
||||
}
|
||||
if (!Configure::check('password_auth.enabled') || Configure::read('password_auth.enabled')) {
|
||||
$showPasswordField = true;
|
||||
}
|
||||
echo $this->element('genericElements/Form/genericForm', [
|
||||
'data' => [
|
||||
'description' => __('Roles define global rules for a set of users, including first and foremost access controls to certain functionalities.'),
|
||||
|
@ -62,7 +66,7 @@
|
|||
'required' => $passwordRequired,
|
||||
'autocomplete' => 'new-password',
|
||||
'value' => '',
|
||||
'requirements' => (bool)$passwordRequired
|
||||
'requirements' => $showPasswordField,
|
||||
],
|
||||
[
|
||||
'field' => 'confirm_password',
|
||||
|
@ -70,7 +74,7 @@
|
|||
'type' => 'password',
|
||||
'required' => $passwordRequired,
|
||||
'autocomplete' => 'off',
|
||||
'requirements' => (bool)$passwordRequired
|
||||
'requirements' => $showPasswordField,
|
||||
],
|
||||
[
|
||||
'field' => 'role_id',
|
||||
|
|
|
@ -17,7 +17,7 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Filter'),
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
$create_new_allowed = true;
|
||||
$keep_all_allowed = false;
|
||||
$delete_all_allowed = false;
|
||||
$totalAllowed = $create_new_allowed + $keep_all_allowed + $delete_all_allowed;
|
||||
$maxWidth = 99 - ($create_new_allowed ? 33 : 0) - ($keep_all_allowed ? 33 : 0) - ($delete_all_allowed ? 33 : 0);
|
||||
|
||||
$form = $this->element('genericElements/Form/genericForm', [
|
||||
'entity' => null,
|
||||
'ajax' => false,
|
||||
'raw' => true,
|
||||
'data' => [
|
||||
'model' => 'MetaTemplate',
|
||||
'fields' => [
|
||||
[
|
||||
'field' => 'update_strategy',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
['value' => 'create_new', 'text' => 'create_new', 'id' => 'radio_create_new'],
|
||||
['value' => 'keep_both', 'text' => 'keep_both', 'id' => 'radio_keep_both'],
|
||||
['value' => 'delete', 'text' => 'delete', 'id' => 'radio_delete'],
|
||||
],
|
||||
]
|
||||
],
|
||||
'submit' => [
|
||||
'action' => $this->request->getParam('action')
|
||||
],
|
||||
]
|
||||
]);
|
||||
?>
|
||||
|
||||
<div class="conflict-resolution-picker">
|
||||
<div class="mt-3 d-flex justify-content-center">
|
||||
<div class="btn-group justify-content-center" role="group" aria-label="Basic radio toggle button group">
|
||||
<?php if ($create_new_allowed) : ?>
|
||||
<input type="radio" class="btn-check" name="btnradio" id="btnradio1" autocomplete="off" value="create_new" checked>
|
||||
<label class="btn btn-outline-primary mw-<?= $maxWidth ?>" for="btnradio1">
|
||||
<div>
|
||||
<h5 class="mb-3"><?= __('Create new template') ?></h5>
|
||||
<ul class="text-start fs-7">
|
||||
<li><?= __('A new meta-template will be created and made default.') ?></li>
|
||||
<li><?= __('The old meta-template will remain untouched.') ?></li>
|
||||
<li><?= __('Migration of meta-fields to this newer template can be done manually via the UI.') ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($keep_all_allowed) : ?>
|
||||
<input type="radio" class="btn-check" name="btnradio" id="btnradio2" autocomplete="off" value="keep_both">
|
||||
<label class="btn btn-outline-warning mw-<?= $maxWidth ?>" for="btnradio2">
|
||||
<div>
|
||||
<h5 class="mb-3"><?= __('Update non-conflicting') ?></h5>
|
||||
<ul class="text-start fs-7">
|
||||
<li><?= __('Meta-fields not having conflicts will be migrated to the new meta-template.') ?></li>
|
||||
<li><?= __('Meta-fields having a conflicts will stay on their current meta-template.') ?></li>
|
||||
<li><?= __('Conflicts can be taken care of manually via the UI.') ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($delete_all_allowed) : ?>
|
||||
<input type="radio" class="btn-check" name="btnradio" id="btnradio3" autocomplete="off" value="delete">
|
||||
<label class="btn btn-outline-danger mw-<?= $maxWidth ?>" for="btnradio3">
|
||||
<div>
|
||||
<h5 class="mb-3"><?= __('Delete conflicting fields') ?></h5>
|
||||
<ul class="text-start fs-7">
|
||||
<li><?= __('Meta-fields not satisfying the new meta-template definition will be deleted.') ?></li>
|
||||
<li><?= __('All other meta-fields will be upgraded to the new meta-template.') ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-none conflict-resolution-form-container">
|
||||
<?= $form ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const $form = $('.conflict-resolution-form-container form')
|
||||
const $create = $form.find('input#radio_create_new')
|
||||
const $keep = $form.find('input#radio_keep_both')
|
||||
const $delete = $form.find('input#radio_delete')
|
||||
|
||||
$(document).ready(function() {
|
||||
$('.conflict-resolution-picker').find('input[type="radio"]').change(function() {
|
||||
updateSelected($(this).val())
|
||||
})
|
||||
updateSelected('create_new')
|
||||
})
|
||||
|
||||
function updateSelected(choice) {
|
||||
if (choice == 'keep_both') {
|
||||
$keep.prop('checked', true)
|
||||
} else if (choice == 'delete') {
|
||||
$delete.prop('checked', true)
|
||||
} else if (choice == 'create_new') {
|
||||
$create.prop('checked', true)
|
||||
}
|
||||
}
|
||||
}())
|
||||
</script>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue