Yii2: specific form field editable based on Role - php

Well, I can restrict users access permissions(i.e. view, create, update or delete) to forms or views based on access control using behaviors.
But I wonder how I can restrict a specific user from editing some of the fields in the form i.e. allow specific fields is read-only for some users and editable by some users.
Can I provide any kind of access rule in the model or attach some rule in the _form.php itself.
Thanks.

Try this.
if(\Yii::$app->user->can('admin')) {
$form->field($model,'field')->textInput();
}
In this case the input field will only appear if condition matches.

For exactly this case I've created my own class that extends ActiveForm. With the code below it's possible to add rules to a specific field for one or more roles. I use it like this in my forms:
<?= $form->field($model, 'foo', [], [AccessUtil::USER_ROLE => RoleBasedActiveForm::INVISIBLE]) ?>
The Role Based Active Form will show a normal input field when you don't add any rules. It won't display anything if you say it should be invisible for a roles and it also supports read-only (UNEDITABLE).
class RoleBasedActiveForm extends ActiveForm {
const VISIBLE = 0;
const INVISIBLE = 1;
const UNEDITABLE = 2;
public function field($model, $attribute, $options = [], $rules = []) {
$case = empty($rules) ? self::VISIBLE : $this->_validateRules($rules);
switch ($case) {
case self::VISIBLE:
return parent::field($model, $attribute, $options);
case self::INVISIBLE:
return;
case self::UNEDITABLE:
return parent::field($model, $attribute, array_merge($options, [
'template' => '{label}' . $model->$attribute,
]));
}
}
private function _validateRules($rules) {
// validate and return a const
}
}
This will do the form part. You will also have to do some validation after posting the values of course, to make sure someone hasn't modified the form. (changed read only to editable with the inspector or something)

Yes it can be done easily as far ur requirement is concerned without resorting to any utilities.
Try this code:
$form->field($model,'field')->textInput(['disabled' => !\Yii::$app->user->can('admin')]);
you need to replace ur field name and admin with your user role. In the above example only admin can edit this field, for other users it will show as disabled or readonly.
That's it.

This is a better answer. You put this in the validation either inline or as a separate function.
[[ 'input-name' ],
function ($attribute, $params) {
$user_role_array = Yii::$app->authManager->getRolesByUser(Yii::$app->user->getId());
if( !array_key_exists( "Role Name", $user_role_array ) ) {
$myOldA = ( $this->getOldAttribute( $attribute ) );
if( $this->{$attribute} !== (string) $myOldA ) {
$this->addError($attribute, "Please contact XXXXX to modify this option. The field has been reset. You may now resubmit the form" );
$this->{$attribute} = $myOldA;
} //End of if attribute equals old attribute
} //End of if array key exists
}, 'skipOnEmpty' => false, 'skipOnError' => false ],
[Next Rule if inline validation]

Related

How to properly display a calculated value of a non-db field in the ListView?

In Suitecrm/SugarCRM 6.5
I need to modify the ListView. Showing the value of a field based on a certain condition.
In the Account module, the name field may be empty based on certain conditions, I require that when this occurs show the value of a custom field. To do this, first I tryied using 'customCode' + smarty as usual in Edit/Detail vies, but in several posts mention that this is not possible in the ListView. The concession is to use logic hooks + a non-db field to store the calculated field. Following this SO answer I have written the following:
Custom field at custom/Extension/modules/Accounts/Ext/Vardefs/custom_field_name_c.php
<?php
$dictionary['Account']['fields']['name_c']['name'] = 'account_name_c';
$dictionary['Account']['fields']['name_c']['vname'] = 'LBL_ACCOUNT_NAME_C';
$dictionary['Account']['fields']['name_c']['type'] = 'varchar';
$dictionary['Account']['fields']['name_c']['len'] = '255';
$dictionary['Account']['fields']['name_c']['source'] = 'non-db';
$dictionary['Account']['fields']['name_c']['dbType'] = 'non-db';
$dictionary['Account']['fields']['name_c']['studio'] = 'visible';
Label at suitecrm/custom/Extension/modules/Accounts/Ext/Language/es_ES.account_name_c.php
<?php
$mod_strings['LBL_ACCOUNT_NAME_C'] = 'Nombre de cuenta';
Logic hook entry at custom/modules/Accounts/logic_hooks.php
$hook_array['process_record'] = Array();
$hook_array['process_record'][] = Array(
2,
'Get proper name',
'custom/modules/Accounts/hooks/ListViewLogicHook.php',
'ListViewLogicHook',
'getProperName'
);
Logic hook at custom/modules/Accounts/hooks/ListViewLogicHook.php
<?php
class ListViewLogicHook
{
public function getProperName(&$bean, $event, $arguments)
{
if (empty($bean->fetched_row['name'])) {
$bean->account_name_c = $bean->fetched_row['person_name_c'];
} else {
$bean->account_name_c = $bean->fetched_row['name'];
}
// Here I also tried, but same result
// $bean->account_name_c = empty($bean->name) ? $bean->person_name_c : $bean->name;
}
}
listviewdefs.php at custom/modules/Accounts/metadata/listviewdefs.php
I added the field
'NAME_C' =>
array (
'width' => '20%',
'label' => 'LBL_ACCOUNT_NAME_C',
'default' => true,
),
After these modifications and repair I hope to see the field full with the right value. But it appears empty. I have verified that the logic hook is properly called, but the name andperson_name_c fields are always empty. In case it is relevant I have verified in Studio that the name fields in Account is typename I do not know what this means when it comes to obtaining its value.
I appreciate your comments
The problem is in the logic hook is due to first that the $bean does not have access to the custom fields, so I have to invoke them using $bean->custom_fields->retrieve(); Also the name field is always empty, I had to use DBManager to get only the name field.
The logic of the final logic hook is the following:
<?php
class ListViewLogicHook
{
public function getProperName($bean, $event, $arguments)
{
// Get access to custom fields from $bean
$bean->custom_fields->retrieve();
// Get access to name property using DBManager because $bean->name return null
$sql = "SELECT name FROM accounts WHERE id = '{$bean->id}'";
$name = $GLOBALS['db']->getOne($sql);
// Assign a value to non-db field
$bean->name_c = empty($name) ? $bean->nombre_persona_c : $name;
}
}
I was not familiar with the method $bean->custom_fields->retrieve() and at the moment I do not know why is empty the name field and I understand others fields remain empty.
I hope this is useful
We achieved that doing an after_retrieve hook, very fast and simple - This is taken from a working example.
public function RemoveCost(&$bean, $event, $arguments)
{
global $current_user;
include_once(‘modules/ACLRoles/ACLRole.php’);
$roles = ACLRole::getUserRoleNames($current_user->id);
if(!array_search("CanSeeCostRoleName",$roles)){
$bean->cost = 0;
$bean->cost_usdollar = 0;
}
}
All you need is to define and add this function to the module logic_hooks.php
You can even tailor down to specific calls:
if (isset($_REQUEST['module'],$_REQUEST['action']) && $_REQUEST['module'] == 'Opportunities' && $_REQUEST['action'] == 'DetailView')
As some times there are views you do want to show the field, for example quicksearch popup.

CakePHP 3.0.8 translate behavior and data validation (requirePresence, notEmpty)

My problem is simple, yet I can't figure out how to solve it.
My website is multilanguage. I want the user to be able to add an article in multiple language if he wants, while requiring the inputs of his language (depending on his locale).
Problem is, with CakePHP's conventions about translation, all the inputs must end with the field's name, no matter what language. So all the fields has the same rule for the same field. I can't make one "name" required while another in another language not required.
For example, the default language's input would be:
<input type="text" name="name" required="required" maxlength="45" id="name">
And below that, another language's input for the same field:
<input type="text" name="locales[fr_CA][name]" required="required" maxlength="45" id="locales-fr-ca-name">
The "required" attribute is automatically added to both because of these rules:
$validator
->requirePresence('name', 'create')
->notEmpty('name')
->add('name', [
'length' => [
'rule' => ['minLength', 10],
'message' => 'The title needs to be at least 10 characters long.',
]
]);
Note: I have to change the locale to the default (en_US) when I save to be able to save in multiple languages + the default language (otherwise the default inputs are saved in the default table AND in the i18n table).
if ($this->request->is('post')) {
I18n::locale('en_US');
// ......
EDIT: So here's the complete piece of code when I save (IngredientsController.php)
public function add() {
$ingredient = $this->Ingredients->newEntity();
if ($this->request->is('post')) {
$ingredient = $this->Ingredients->patchEntity($ingredient, $this->request->data);
if(isset($this->request->data['locales'])) {
foreach ($this->request->data['locales'] as $lang => $data) {
$ingredient->translation($lang)->set($data, ['guard' => false]);
}
}
$locale = I18n::locale(); // At this point the locale is fr_CA (not de default)
I18n::locale('en_US'); // Change the locale to the default
if ($this->Ingredients->save($ingredient)) {
$this->Flash->success(__('The ingredient has been saved.'));
I18n::locale($locale); // Put the locale back to the user's locale
return $this->redirect(['action' => 'index']);
} else {
I18n::locale($locale);
$this->Flash->error(__('The ingredient could not be saved. Please, try again.'));
}
}
$this->set(compact('ingredient'));
$this->set('_serialize', ['ingredient']);
}
I set the default locale is the bootstrap.php
/**
* Set the default locale. This controls how dates, number and currency is
* formatted and sets the default language to use for translations.
*/
ini_set('intl.default_locale', 'en_US');
Configure::write('Config.locales', ['fr_CA']);
I determine the user's locale in the AppController.php
public function beforeFilter(Event $event)
{
$locales = Configure::read('Config.locales');
$boom = explode(',', str_replace('-', '_', $_SERVER['HTTP_ACCEPT_LANGUAGE']));
$user_lang = substr($boom[0], 0, 2);
// This piece of code is only to change the locale to fr_CA even if the user's language is just fr or fr_FR
if(in_array($user_lang, Configure::read('Config.langs'))) {
if(in_array($boom[0], $locales)) {
I18n::locale($boom[0]);
} else {
foreach ($locales as $locale) {
if(substr($locale, 0, 2) == $user_lang) {
I18n::locale($locale);
}
}
}
}
$this->set('locales', $locales);
$this->set('locale', I18n::locale());
}
So if I save while being in a different locale than the default, the same default inputs will be saved in the ingredients table AND in the i18n table in fr_CA
Defaults saved in translation table
The fact that the input for the default language is being stored in the translation table in case the default locale has been changed, seems to be the expected behavior, just like when reading data where it will retrieve the data with respect to the current locale, the same applies when saving data.
Cookbook > Database Access & ORM > Behaviours > Translate > Saving in Another Language
Changing the locale to the default is a workaround, but it might be a little too invasive, as it will interfer with any code that uses that value to check the current locale. It's better to directly set the desired locale on the table
$Ingredients->locale(I18n::defaultLocale());
or, which is the least invasive option, on the main entity instead
$ingredient->_locale = I18n::defaultLocale();
Also the former is what the linked docs sesction is describing, but not actually showing, that needs to be fixed.
Fields picking up "wrong" validation rules
While I can see why the form helper, respectively the entity context, picks up validation rules for the "wrong" fields, ie xyz.name fields pick up those for the name field, I can't tell whether this is how it is ment to work.
https://github.com/cakephp/cakephp/blob/3.0.10/src/View/Form/EntityContext.php#L394
https://github.com/cakephp/cakephp/blob/3.0.10/src/View/Form/EntityContext.php#L439
Since it wouldn't pick up nested errors, I guess this is the expected behavior, but I'm not sure, so I'd suggest to create an issue over at GitHub for clarification. In any case, there are various ways to work around this, for example by renaming the fields, or by setting the required option to false.
echo $this->Form->input('locales.fr_CA.name', [
// ...
'required' => false
]);
In your example this is pretty much just a frontend issue, as the fields are not going to be actually validated on the server side.
Another option would be to use a custom translation table class, with validation specific to translations, that actually apply to the used fields, however this is probably not that advisable unless you actually want to apply any validation at all.
Apply validation/application rules to translated columns
For the sake of completion, let's cover validation/application rules too.
In order to actually apply validation and/or application rules, and have them recognized in forms, you'll have use a custom translation table class that holds the rules, and you must use the actual property name that the translate behavior uses for the hasMany associated translation table, which is _i18n.
Here's an example.
src/Model/Table/IngredientsI18nTable.php
namespace App\Model\Table;
use Cake\Datasource\EntityInterface;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
class IngredientsI18nTable extends Table
{
public function initialize(array $config) {
$this->entityClass('Ingredient');
$this->table('i18n');
$this->displayField('id');
$this->primaryKey('id');
}
public function validationDefault(Validator $validator) {
$validator
->allowEmpty('name')
->add('name', 'valid', [
'rule' => function ($value, $context) {
return false;
}
]);
return $validator;
}
public function buildRules(RulesChecker $rules)
{
$rules->add(
function (EntityInterface $entity, $options) {
return false;
},
'i18nName',
[
'errorField' => 'name'
]
);
return $rules;
}
}
IngredientsTable
public function initialize(array $config) {
// ...
$this->addBehavior('Translate', [
// ...
'translationTable' => 'IngredientsI18n'
]);
}
View template
echo $this->Form->hidden('_i18n.0.locale', ['value' => 'fr_FR']);
echo $this->Form->input('_i18n.0.name');
echo $this->Form->hidden('_i18n.1.locale', ['value' => 'da_DK']);
echo $this->Form->input('_i18n.1.name');
// ...
Now the fields will pick up the correct validator, and thus are not being marked as required. Also validation will be applied when creating/patching entities, and finally application rules are being applied too. However I can't guarantee that this doesn't have any side effects, as the Translate behavior internally doesn't seem to account for the situation that the _i18n property has been set externally!
Also you'll still have to set the translations on the entity using translations() in order for the translations to be saved correctly!
foreach ($this->request->data['_i18n'] as $translation) {
$ingredient->translation($translation['locale'])->set('name', $translation['name']);
}

Laravel - both input values can't be no how to validate?

I'm using Laravel for a project and want to know how to validate a particular scenario I'm facing. I would like to do this with the native features of Laravel if this is possible?
I have a form which has two questions (as dropdowns), for which both the answer can either be yes or no, however it should throw a validation error if both of the dropdowns equal to no, but they can both be yes.
I've check the laravel documentation, but was unsure what rule to apply here, if there is one at all that can be used? Would I need to write my own rule in this case?
very simple:
let's say both the fields names are foo and bar respectively.
then:
// Validate for those fields like $rules = ['foo'=>'required', 'bar'=>'required'] etc
// if validation passes, add this (i.e. inside if($validator->passes()))
if($_POST['foo'] == 'no' && $_POST['bar'] == 'no')
{
$messages = new Illuminate\Support\MessageBag;
$messages->add('customError', 'both fields can not be no');
return Redirect::route('route.name')->withErrors($validator);
}
the error messge will appear while retrieving.
if you get confuse, just dump the $error var and check how to retrieve it. even if validation passes but it gets failed in the above code, it won't be any difference than what would have happened if indeed validation failed.
Obviously don't know what your form fields are called, but this should work.
This is using the sometimes() method to add a conditional query, where the field value should not be no if the corresponding field equals no.
$data = array(
'field1' => 'no',
'field2' => 'no'
);
$validator = Validator::make($data, array());
$validator->sometimes('field1', 'not_in:no', function($input) {
return $input->field2 == 'no';
});
$validator->sometimes('field2', 'not_in:no', function($input) {
return $input->field1 == 'no';
});
if ($validator->fails()) {
// will fail in this instance
// changing one of the values in the $data array to yes (or anything else, obvs) will result in a pass
}
Just to note, this will only work in Laravel 4.2+

Symfony 1.4 conditional validation

I have a Profile form that inherits from sfGuardRegisterForm
I have these fields:
$this->useFields(
array('first_name',
'last_name',
'email_address',
'country',
'language',
'current_password',
'new_password',
'password_again',
)
);
Required fields are:
email_address, country and language
And the conditions are:
If the email_address is not equal with the current email_address
then check if it's unique then save it
If the current_password is the actual password of the user then verify if new_password and password_again are equals and verify that the new_password is not equal to the actual password of the user
I just can't figure out in how implement this
EDIT
Thanks 1ed your example works but the problem is that I load the user Profile and I fill the fields: 'first_name', 'last_name', 'email_address', 'country', 'language' with the actual logged user so the email_address field will show the email address:
//...
$this->widgetSchema['email_address']->setDefault($this->_user->getEmailAddress());
//...
If the user dont change the email it will always show this message:
An object with the same "email_address" already exist.
I just want to skip that
Also this $this->getObject()->checkPassword() does not works, always show this message:
Incorrect current password.
I use:
$this->_user = sfContext::getInstance()->getUser()->getGuardUser();
To get actual user profile
EDIT2
Thanks again 1ed
This is very weird and I'm getting frustated, this is the situation
I have a "workaround" for this but it does not follow the standard, I can make it works but using sfContext::getInstance()->getUser()->getGuardUser(); and it will be more unnecesary code
If I use new ProfileForm($user) automatically fills all the fields, that's very good but I can't setDefault() I can't set null or empty any field so I can't use doUpdateObject() because this function only works when the current data is updated, also I have tested overriding bind(), save() etc. without results
email_address uniqueness: you should set unique: true in schema, in sfDoctrineGuardPlugin that's the case by default, so in BasesfGuardUserForm you should see a unique validator already: sfValidatorDoctrineUnique(array('model' => 'sfGuardUser', 'column' => array('email_address'))
current_password: you should create a callback type post validator for this
// in sfGuardRegisterForm::configure()
// these fields can be blank
$this->getValidator('current_password')->setOption('required', false);
$this->getValidator('new_password')->setOption('required', false);
$this->getValidator('password_again')->setOption('required', false);
// check the current password (this validator is not `required` by default)
$this->mergePostValidator(new sfValidatorCallback(array(
'callback' => array($this, 'checkPassword'),
), array(
'invalid' => 'Incorrect current password.'
)));
// add this method to the same form class
public function checkPassword(sfValidatorBase $validator, array $values, array $arguments)
{
// if a new password is given check whether the old one is correct or not and rise an error if not correct
if(0 != strlen($values['new_password']) && !$this->getObject()->checkPassword($values['current_password']))
{
throw new sfValidatorErrorSchema($validator, array(
'current_password' => new sfValidatorError($validator, 'invalid')
));
}
return $values;
}
Alternatively you can create a custom post validator, but I think it's not necessary.
EDIT:
If you would like to display empty email address just like the password fields add these to your form class:
// at form load remove the default value
protected function updateDefaultsFromObject()
{
parent::updateDefaultsFromObject();
if (isset($this['email_address']))
{
$this->setDefault('email_address', '');
}
}
// before save remove empty email address
protected function doUpdateObject($values)
{
if (isset($values['email_address']) && 0 == strlen($values['email_address']))
{
unset($values['email_address']);
}
parent::doUpdateObject($values);
}
I'll try and explain this in methodical terms instead of giving you a big block of code....
So first, you want to if (email_addr != current_email) and if that's true, go on to do
if (new_pass != current_pass) then follow on to make sure if (new_pass == new_pass_again)
Inside all of these IFs, you can return a true/false or some kind of flag, or just //do code inside the brackets :p
EDIT: encase these IFs in: if (country != NULL && language != NULL && email_addr != NULL)

My choice of fieldname for CakePHP's form helper is not working properly, why?

I'm creating an AJAX form. The problem is when I'm trying to create a input form with formhelper, my input's name attribute is not correctly renedered in the view. Here's my code:
$form->input('MainAttribute.'.$i.'.SubAttribute.'.$j.'.score', array('label' => '', 'options' => $scores));
I created it that way because I want SubAttribute to be inside MainAttribute. When I inspect the HTML, the name attribute of the form is cutted of like:
name="data[SuperMainAttribute]"
How can I specify the name attribute to the one that I'm planning on doing?
(e.g. data[MainAttribute][0][SubAttribute][0][score])
Edit:
Here are my model relationships:
Control hasMany MainAttribute
MainAttribute hasMany SubAttribute
The ctp is in a view of the Control Controller
In general, almost anytime you call FormHelper::input, the first parameter will appear in one of the following formats:
for the primary model, or hasOne and belongsTo associations: $form->input('Model.field')
for hasMany associations: $form->input("Model.{$n}.field")
for hasAndBelongsToMany associations: $form->input("Model.Model.{$n}.field")
(In these cases, $n is an iterator (0,1,2,3, etc.), allowing you to add multiple records to hasMany- and hasAndBelongsToMany-associated models.)
Your specific case is tricky, because you want to save a Control record, and all of its MainAttribute records, and all of each MainAttribute's SubAttribute records. This isn't possible without some data manipulation in the controller. The way I'd probably tackle this problem is the following.
In the view:
echo $form->create('Control', array('action'=>'add'));
echo $form->input('Control.field_name');
$iLimit = 4;
$jLimit = 2;
for ($k=$i=0;$i<$iLimit;$i++) {
echo $form->input("MainAttribute.{$i}.field_name");
for ($j=0;$j<$jLimit;$j++) {
echo $form->input("SubAttribute.{$k}.ixMainAttribute", array('type'=>'hidden','value'=>$i));
echo $form->input("SubAttribute.{$k}.field_name");
$k++;
}
}
echo $form->end('Submit');
In ControlsController:
function add()
{
if (! empty($this->data)) {
// Perform data validation separately...
if ( $this->Control->save( $this->data['Control'], false )) {
foreach ( $this->data['MainAttribute'] as $k => $_data ) {
$_subAttributes = Set::extract("/SubAttribute[ixMainAttribute={$k}]", $this->data);
$insert = array(
'MainAttribute' => am( $_data, array('control_id' => $this->Control->id)),
'SubAttribute' => $_subAttributes
);
$this->Control->MainAttribute->saveAll($insert, array('validate'=>false));
}
}
}
}
HTH.

Categories