Merge branch 'develop'

debug-branch v1.11
iglocska 2023-01-18 10:10:21 +01:00
commit 108b778857
No known key found for this signature in database
GPG Key ID: BEA224F1FEF113AC
15 changed files with 425 additions and 52 deletions

View File

@ -0,0 +1,283 @@
<?php
namespace App\Command;
use Cake\Console\Command;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
use Cake\Filesystem\File;
use Cake\Utility\Hash;
use Cake\Core\Configure;
use Cake\Utility\Security;
class FastUserEnrolmentCommand extends Command
{
protected $modelClass = 'Alignments';
private $autoYes = false;
private $alignment_type = null;
private $individual_email_column = false;
private $organisation_name_column = false;
private $create_user = false;
private $role_id = null;
protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser->setDescription('Create alignements (and optionally enroll users) based on the provided CSV file.');
$parser->addArgument('path', [
'help' => 'A path to the source file that should be used to create the alignments.',
'required' => true
]);
$parser->addOption('alignment_type', [
'short' => 't',
'help' => 'The alignment type to use',
'default' => 'member',
]);
$parser->addOption('individual_email_column', [
'short' => 'i',
'help' => 'The name of the column to find the individual email address',
'default' => 'Email',
]);
$parser->addOption('organisation_name_column', [
'short' => 'o',
'help' => 'The name of the column to find the organisation name',
'default' => 'TeamName',
]);
$parser->addOption('create_user', [
'short' => 'c',
'help' => 'Should the user be created',
'boolean' => true,
'default' => false,
]);
$parser->addOption('role_id', [
'short' => 'r',
'help' => 'The role to assign to the user',
]);
$parser->addOption('yes', [
'short' => 'y',
'help' => 'Automatically assume yes to any prompts',
'default' => false,
'boolean' => true
]);
return $parser;
}
public function execute(Arguments $args, ConsoleIo $io)
{
$this->io = $io;
$path = $args->getArgument('path');
$this->alignment_type = $args->getOption('alignment_type');
$this->individual_email_column = $args->getOption('individual_email_column');
$this->organisation_name_column = $args->getOption('organisation_name_column');
$this->create_user = $args->getOption('create_user');
$this->role_id = $args->getOption('role_id');
$this->autoYes = $args->getOption('yes');
$alignmentTable = $this->modelClass;
$this->loadModel($alignmentTable);
$data = $this->getDataFromFile($path);
$updatedData = $this->updateBeforeSave($data);
$alignmentEntities = $this->marshalData($this->{$alignmentTable}, $updatedData);
$alignmentEntitiesSample = array_slice($alignmentEntities, 0, min(5, count($alignmentEntities)));
$ioTable = $this->transformEntitiesIntoTable($alignmentEntitiesSample);
if ($this->autoYes) {
$this->saveAligmentData($this->{$alignmentTable}, $alignmentEntities);
} else {
$io->helper('Table')->output($ioTable);
$selection = $io->askChoice('A sample of the data you about to be saved is provided above. Would you like to proceed?', ['Y', 'N'], 'N');
if ($selection == 'Y') {
$this->saveAligmentData($this->{$alignmentTabble}, $alignmentEntities);
}
}
if ($this->create_user) {
$this->loadModel('Users');
if (is_null($this->role_id)) {
$defaultRole = $this->Users->Roles->find()->select(['id'])->where(['is_default' => true])->first();
if (empty($defaultRole)) {
$this->io->error(__('No default role available. Create a default role or provide the role ID to be assigned.'));
die(1);
}
$defaultRole = $defaultRole->toArray();
if (!empty($defaultRole['perm_admin'])) {
$selection = $io->askChoice('The default role has the `admin` permission. Confirm giving the admin permission to users to be enrolled.', ['Y', 'N'], 'N');
if ($selection != 'Y') {
die(1);
}
}
$this->role_id = $defaultRole['id'];
} else {
$role = $this->Users->Roles->find()->select(['id'])->where(['id' => $this->role_id])->first();
if (empty($role)) {
$this->io->error(__('Provided role ID does not exist'));
die(1);
}
}
$userEntities = $this->createEntitiesForUsers($alignmentEntities);
if ($this->autoYes) {
$this->enrolUsers($userEntities);
} else {
$userEntitiesSample = array_slice($userEntities, 0, min(5, count($userEntities)));
$ioTable = $this->transformEntitiesIntoTable($userEntitiesSample);
$io->helper('Table')->output($ioTable);
$selection = $io->askChoice('A sample of the data you about to be saved is provided above. Would you like to proceed?', ['Y', 'N'], 'N');
if ($selection == 'Y') {
$this->enrolUsers($userEntities);
}
}
}
}
private function saveAligmentData($alignmentTable, $entities)
{
$this->io->verbose('Saving data');
$saveResult = $alignmentTable->saveMany($entities);
if ($saveResult === false) {
$errors = [];
$errorCount = 0;
foreach ($entities as $entity) {
$errorCount += 1;
$errors[json_encode($entity->getErrors())] = true;
}
$this->io->error(__('{0} Errors while saving data', $errorCount));
$this->io->error(json_encode(array_keys($errors)));
$this->io->success(__('Saved {0} aligments', count($entities) - $errorCount));
}
}
private function enrolUsers($entities)
{
$this->io->verbose('Saving data');
$errors = [];
$errorCount = 0;
foreach ($entities as $entity) {
$succes = $this->Users->save($entity);
if (empty($succes)) {
$errorCount += 1;
$errors[json_encode($entity->getErrors())] = true;
} else {
if (Configure::read('keycloak.enabled')) {
$this->Users->enrollUserRouter($succes);
}
}
}
if (!empty($errors)) {
$this->io->error(__('{0} Errors while saving data', $errorCount));
$this->io->error(json_encode(array_keys($errors)));
}
$this->io->success(__('Enrolled {0} users', count($entities) - $errorCount));
}
private function createEntitiesForUsers($alignmentEntities)
{
$entities = [];
foreach ($alignmentEntities as $alignmentEntity) {
$entity = $this->Users->newEntity([
'individual_id' => $alignmentEntity->individual_id,
'organisation_id' => $alignmentEntity->organisation_id,
'username' => $alignmentEntity->individual_email,
'password' => Security::randomString(20),
'role_id' => $this->role_id,
]);
$entities[] = $entity;
}
return $entities;
}
private function marshalData($alignmentTable, $data)
{
$entities = $alignmentTable->newEntities($data, [
'accessibleFields' => ($alignmentTable->newEmptyEntity())->getAccessibleFieldForNew()
]);
return $entities;
}
private function updateBeforeSave($data)
{
$this->loadModel('Individuals');
$this->loadModel('Organisations');
$updatedData = [];
foreach ($data as $entry) {
$new = [
'individual_id' => $this->getIndividualByEmail($entry[$this->individual_email_column]),
'organisation_id' => $this->getOrganisationsByName($entry[$this->organisation_name_column]),
'type' => $this->alignment_type,
'individual_email' => $entry[$this->individual_email_column],
];
if (empty($new['organisation_id'])) {
$this->io->error("Error while parsing source data. Could not find organisation with name: " . $entry[$this->organisation_name_column]);
die(1);
}
if (empty($new['individual_id'])) {
$this->io->error("Error while parsing source data. Could not find individuals with email: " . $entry[$this->individual_email_column]);
die(1);
}
$new['individual_id'] = $new['individual_id']['id'];
$new['organisation_id'] = $new['organisation_id']['id'];
$updatedData[] = $new;
}
return $updatedData;
}
private function getIndividualByEmail($email)
{
return $this->Individuals->find()->where([
'email' => $email,
])->first()->toArray();
}
private function getOrganisationsByName($name)
{
return $this->Organisations->find()->where([
'name' => $name,
])->first()->toArray();
}
private function getDataFromFile($path)
{
$file = new File($path);
if ($file->exists()) {
$this->io->verbose('Reading file');
$text = $file->read();
$file->close();
if (!empty($text)) {
$rows = array_map('str_getcsv', explode(PHP_EOL, $text));
if (count($rows[0]) != count($rows[1])) {
$this->io->error('Error while parsing source data. CSV doesn\'t have the same number of columns');
die(1);
}
$csvData = [];
$headers = array_shift($rows);
foreach ($rows as $row) {
if (count($headers) == count($row)) {
$csvData[] = array_combine($headers, $row);
}
}
return $csvData;
}
}
return false;
}
private function transformEntitiesIntoTable($entities, $header = [])
{
$table = [[]];
if (!empty($entities)) {
$tableHeader = empty($header) ? array_keys(Hash::flatten($entities[0]->toArray())) : $header;
$tableContent = [];
foreach ($entities as $entity) {
$row = [];
foreach ($tableHeader as $key) {
if (in_array($key, $entity->getVirtual())) {
continue;
}
$row[] = (string) $entity[$key];
}
$tableContent[] = $row;
}
$table = array_merge([$tableHeader], $tableContent);
}
return $table;
}
}

View File

@ -87,11 +87,10 @@ class FieldSquasherCommand extends Command
if ($selection == 'Y') {
$this->saveDataOnDisk($filename, $candidateResult['candidates']);
}
die(1);
$entities = $candidateResult['candidates'];
$selection = $io->askChoice('A sample of the data you about to be saved is provided above. Would you like to proceed?', ['Y', 'N'], 'N');
if ($selection == 'Y') {
// $this->saveData($this->{$table}, $entities);
$this->saveData($this->{$table}, $entities);
}
}

View File

@ -190,7 +190,7 @@ class ImporterCommand extends Command
if (is_null($metaEntity)) {
$metaEntity = $this->MetaFields->newEmptyEntity();
$metaEntity->field = $fieldName;
$metaEntity->scope = $table->metaFields;
$metaEntity->scope = $table->getBehavior('MetaFields')->getScope();
$metaEntity->meta_template_id = $metaTemplate->id;
if (isset($metaTemplateFieldsMapping[$fieldName])) { // a meta field template must exists
$metaEntity->meta_template_field_id = $metaTemplateFieldsMapping[$fieldName];
@ -248,6 +248,7 @@ class ImporterCommand extends Command
{
foreach ($entity->metaFields as $i => $metaEntity) {
$metaEntity->parent_id = $entity->id;
$metaEntity->setNew(true);
if ($metaEntity->hasErrors() || is_null($metaEntity->value)) {
$this->io->error(json_encode(['entity' => $metaEntity, 'errors' => $metaEntity->getErrors()], JSON_PRETTY_PRINT));
unset($entity->metaFields[$i]);
@ -289,35 +290,35 @@ class ImporterCommand extends Command
$values = array_map("self::{$fieldConfig['massage']}", $values);
}
if (isset($defaultFields[$key])) {
$data[$key] = $values;
$data[$key] = array_map('trim', $values);
} else {
$data['metaFields'][$key] = $values;
$data['metaFields'][$key] = array_map('trim', $values);
}
}
return $this->invertArray($data);
}
private function extractDataFromCSV($defaultFields, $config, $source)
{
$csvData = $this->csvToAssociativeArray($source);
return $this->extractDataFromJSON($defaultFields, $config, $csvData);
}
private function csvToAssociativeArray($source): array
{
$rows = array_map('str_getcsv', explode(PHP_EOL, $source));
if (count($rows[0]) != count($rows[1])) {
$this->io->error('Error while parsing source data. CSV doesn\'t have the same number of columns');
die(1);
}
$header = array_shift($rows);
$data = array();
foreach($rows as $row) {
$dataRow = [];
foreach ($header as $i => $headerField) {
if (isset($defaultFields[$headerField])) {
$dataRow[$headerField] = $row[$i];
} else {
$dataRow['metaFields'][$headerField] = $row[$i];
}
$csvData = [];
$headers = array_shift($rows);
foreach ($rows as $row) {
if (count($headers) == count($row)) {
$csvData[] = array_combine($headers, $row);
}
$data[] = $dataRow;
}
return $data;
return $csvData;
}
private function lockAccess(&$entity)
@ -477,6 +478,9 @@ class ImporterCommand extends Command
foreach ($entities as $entity) {
$row = [];
foreach ($tableHeader as $key) {
if (in_array($key, $entity->getVirtual())) {
continue;
}
$subKeys = explode('.', $key);
if (in_array('metaFields', $subKeys)) {
$found = false;

View File

@ -0,0 +1,48 @@
<?php
namespace App\Command;
use Cake\Console\Command;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
use Cake\Filesystem\File;
use Cake\Utility\Hash;
use Cake\Core\Configure;
class MetaTemplateCommand extends Command
{
protected $modelClass = 'MetaTemplates';
protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser->setDescription('Load and enable the provided meta-template');
$parser->addArgument('uuid', [
'help' => 'The UUID of the meta-template to load and enable',
'required' => true
]);
return $parser;
}
public function execute(Arguments $args, ConsoleIo $io)
{
$this->io = $io;
$template_uuid = $args->getArgument('uuid');
$metaTemplateTable = $this->modelClass;
$this->loadModel($metaTemplateTable);
$result = $this->MetaTemplates->createNewTemplate($template_uuid);
if (empty($result['success'])) {
$this->io->error(__('Could not create meta-template'));
$this->io->error(json_encode($result));
die(1);
}
$template = $this->MetaTemplates->find()->where(['uuid' => $template_uuid])->first();
if (!empty($template)) {
$template->enabled = true;
$success = $this->MetaTemplates->save($template);
if (!empty($success)) {
$this->io->success(__('Meta-template loaded and enabled'));
}
}
}
}

View File

@ -105,7 +105,7 @@ class AlignmentsController extends AppController
}
}
if ($scope === 'organisations') {
$individuals = $this->Individuals->find('list', ['valueField' => 'email']);
$individuals = $this->Individuals->find('list', ['valueField' => 'email'])->toArray();
$this->set('individuals', $individuals);
$organisation = $this->Organisations->find()->where(['id' => $source_id])->first();
if (empty($organisation)) {
@ -113,7 +113,7 @@ class AlignmentsController extends AppController
}
$this->set(compact('organisation'));
} else {
$organisations = $this->Organisations->find('list', ['valueField' => 'name']);
$organisations = $this->Organisations->find('list', ['valueField' => 'name'])->toArray();
$this->set('organisations', $organisations);
$individual = $this->Individuals->find()->where(['id' => $source_id])->first();
if (empty($individual)) {

View File

@ -45,7 +45,7 @@ class BroodsController extends AppController
$dropdownData = [
'organisation' => $this->Organisations->find('list', [
'sort' => ['name' => 'asc']
])
])->toArray()
];
$this->set(compact('dropdownData'));
}
@ -72,7 +72,7 @@ class BroodsController extends AppController
$dropdownData = [
'organisation' => $this->Organisations->find('list', [
'sort' => ['name' => 'asc']
])
])->toArray()
];
$this->set(compact('dropdownData'));
$this->render('add');

View File

@ -142,6 +142,9 @@ class CRUDComponent extends Component
$data[$i] = $this->attachMetaTemplatesIfNeeded($row, $metaTemplates);
}
$this->Controller->set('meta_templates', $metaTemplates);
$this->Controller->set('meta_templates_enabled', array_filter($metaTemplates, function($template) {
return $template['enabled'];
}));
}
if (true) { // check if stats are requested
$modelStatistics = [];

View File

@ -98,7 +98,7 @@ class AuthKeycloakBehavior extends Behavior
public function getUserIdByUsername(string $username)
{
$response = $this->restApiRequest(
'%s/admin/realms/%s/users/?username=' . urlencode($username),
'%s/admin/realms/%s/users/?username=' . $this->urlencodeEscapeForSprintf($username),
[],
'GET'
);

View File

@ -4,6 +4,7 @@ namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\ORM\Table;
use Cake\ORM\RulesChecker;
use Cake\Validation\Validator;
class AlignmentsTable extends AppTable
@ -24,7 +25,16 @@ class AlignmentsTable extends AppTable
->notEmptyString('organisation_id')
->requirePresence(['individual_id', 'organisation_id'], 'create');
return $validator;
}
}
public function buildRules(RulesChecker $rules): RulesChecker
{
$rules->add($rules->isUnique(
['individual_id', 'organisation_id', 'type'],
__('This alignment already exists.')
));
return $rules;
}
public function setAlignment($organisation_id, $individual_id, $type): void
{

View File

@ -63,12 +63,15 @@ class PermissionLimitationsTable extends AppTable
])->count();
}
if (isset($data['global'])) {
$conditions = [
'scope' => 'user',
'field' => $field,
];
if (!empty($ownOrgUserIds)) {
$conditions['parent_id IN'] = array_values($ownOrgUserIds);
}
$limitations[$field]['organisation']['current'] = $MetaFields->find('all', [
'conditions' => [
'scope' => 'user',
'field' => $field,
'parent_id IN' => array_values($ownOrgUserIds)
]
'conditions' => $conditions,
])->count();
}
}

View File

@ -1,4 +1,4 @@
{
"version": "1.10",
"version": "1.11",
"application": "Cerebrate"
}

View File

@ -2008,6 +2008,12 @@ class BoostrapDropdownMenu extends BootstrapGeneric
}
$classes = ['dropdown-item'];
if (!empty($entry['class'])) {
if (!is_array($entry['class'])) {
$entry['class'] = [$entry['class']];
}
$classes = array_merge($classes, $entry['class']);
}
$params = ['href' => '#'];
if (!empty($entry['menu'])) {

View File

@ -10,7 +10,8 @@ echo $this->element('genericElements/Form/genericForm', array(
array(
'field' => ($scope === 'individuals' ? 'organisation_id' : 'individual_id'),
'options' => ($scope === 'individuals' ? $organisations : $individuals),
'type' => 'select'
'type' => 'dropdown',
'select2' => true,
),
array(
'field' => 'type'

View File

@ -19,6 +19,9 @@ if (!empty($fieldData['label'])) {
if ($controlParams['options'] instanceof \Cake\ORM\Query) {
$controlParams['options'] = $controlParams['options']->all()->toList();
}
if (!empty($fieldData['select2'])) {
$controlParams['class'] .= ' select2-input';
}
if (in_array('_custom', array_keys($controlParams['options']))) {
$customInputValue = $this->Form->getSourceValue($fieldData['field']);
if (!in_array($customInputValue, $controlParams['options'])) {
@ -49,6 +52,15 @@ echo $this->FormFieldMassage->prepareFormElement($this->Form, $controlParams, $f
$select.attr('onclick', 'toggleFreetextSelectField(this)')
$select.parent().find('input.custom-value').attr('oninput', 'updateAssociatedSelect(this)')
updateAssociatedSelect($select.parent().find('input.custom-value')[0])
<?php if (!empty($fieldData['select2'])) : ?>
let $container = $select.closest('.modal-dialog')
if ($container.length == 0) {
$container = $(document.body)
}
$select.select2({
dropdownParent: $container,
})
<?php endif; ?>
})
})()

View File

@ -19,27 +19,31 @@ $availableColumnsHtml = $this->element('/genericElements/ListTopBar/group_table_
$metaTemplateColumnMenu = [];
if (!empty($meta_templates)) {
$metaTemplateColumnMenu[] = ['header' => true, 'text' => __('Meta Templates'), 'icon' => 'object-group',];
foreach ($meta_templates as $meta_template) {
$numberActiveMetaField = !empty($tableSettings['visible_meta_column'][$meta_template->id]) ? count($tableSettings['visible_meta_column'][$meta_template->id]) : 0;
$metaTemplateColumnMenu[] = [
'text' => $meta_template->name,
'sup' => $meta_template->version,
'badge' => [
'text' => $numberActiveMetaField,
'variant' => 'secondary',
'title' => __n('{0} meta-field active for this meta-template', '{0} meta-fields active for this meta-template', $numberActiveMetaField, $numberActiveMetaField),
],
'keepOpen' => true,
'menu' => [
[
'html' => $this->element('/genericElements/ListTopBar/group_table_action/hiddenMetaColumns', [
'tableSettings' => $tableSettings,
'table_setting_id' => $data['table_setting_id'],
'meta_template' => $meta_template,
])
]
],
];
if (empty($meta_templates_enabled)) {
$metaTemplateColumnMenu[] = ['header' => false, 'text' => __('- No enabled Meta Templates found -'), 'class' => ['disabled', 'muted']];
} else {
foreach ($meta_templates_enabled as $meta_template) {
$numberActiveMetaField = !empty($tableSettings['visible_meta_column'][$meta_template->id]) ? count($tableSettings['visible_meta_column'][$meta_template->id]) : 0;
$metaTemplateColumnMenu[] = [
'text' => $meta_template->name,
'sup' => $meta_template->version,
'badge' => [
'text' => $numberActiveMetaField,
'variant' => 'secondary',
'title' => __n('{0} meta-field active for this meta-template', '{0} meta-fields active for this meta-template', $numberActiveMetaField, $numberActiveMetaField),
],
'keepOpen' => true,
'menu' => [
[
'html' => $this->element('/genericElements/ListTopBar/group_table_action/hiddenMetaColumns', [
'tableSettings' => $tableSettings,
'table_setting_id' => $data['table_setting_id'],
'meta_template' => $meta_template,
])
]
],
];
}
}
}
$indexColumnMenu = array_merge(