I have a cakephp 2.2 app I'm working on that allows users to submit an expenses claim. The expenese claim is made up of many expenese which have an expense code. The relationships are as follows:
ExpenseClaim hasMany Expense
Expense belongsTo ExpenseClaim
Expense hasAndBelongsToMany ExpenseCode
I want to add validation to ensure when the form is completed the expense code must be complete for each expense but for some reason adding a normal validation rule to the expense model doesnt work, obviously I'm missing something.
Heres the validation in the expense model:
class Expense extends AppModel {
/**
* Display field
*
* #var string
*/
public $displayField = 'id';
/**
* Validation rules
*
* #var array
*/
public $validate = array(
'date' => array(
'notempty' => array(
'rule' => array('notempty'),
'message' => 'Date cannot be blank',
),
),
'sitename' => array(
'notempty' => array(
'rule' => array('notempty'),
'message' => 'Sitename cannot be blank',
),
),
'detail' => array(
'notempty' => array(
'rule' => array('notempty'),
'message' => 'Details cannot be blank',
),
),
'ExpenseCode' => array(
'multiple' => array(
'rule' => array('multiple', array('min' => 1)),
'message' => 'You need to select at least one tag',
),
),
When $this->request->data is submitted via the expenseClaim/add method it looks like this:
/app/Controller/ExpenseClaimsController.php (line 179)
array(
'submit' => 'Save',
'Expense' => array(
(int) 0 => array(
'date' => array(
'day' => '25',
'month' => '03',
'year' => '2013'
),
'sitename' => '123',
'detail' => 'test',
'ExpenseCode' => array(
'ExpenseCode' => ''
),
'billable' => '1',
'amount' => '100',
'miles' => '',
'total' => '100',
'billable_mileage' => '',
'recorded_global_mileage_rate' => '0.4'
)
),
'CashFloat' => array(
'amount' => ''
),
'ExpenseClaim' => array(
'user_id' => '16',
'claim_status_id' => '1',
'date_submitted' => '2013-03-25 18:20:53'
)
)
As you can see the ExpenseCode is empty... How can I ensure that this is not the case?
I managed to solve this by adding the following to my Expense model:
function beforeValidate() {
if (!isset($this->data['ExpenseCode']['ExpenseCode']) || empty($this->data['ExpenseCode']['ExpenseCode'])) {
$this->invalidate('ExpenseCode', 'Type cannot be blank');
}
return true;
}
Hope that helps someone.
Related
I'm trying to slim down my finds queries, by specifying which fields to use. I don't understand how I can specify specific fields for my HABTM fields.
This is my find:
$lastviewed = $this->Lastviewedproduct->find('all', array(
'fields' => array(
'Lastviewedproduct.id', 'Lastviewedproduct.sessionid', 'Lastviewedproduct.product_id',
'Product.id', 'Product.name', 'Product.slug', 'Product.price',
//'Category.id',
//'Mediafile.*',
),
'conditions' => array(
'Lastviewedproduct.status' => 'active',
'Lastviewedproduct.sessionid' => $this->Session->read('Companyname.sessionid'),
'Product.hidden_on_site' => 0,
'Product.visibility' => 1,
'Product.status' => 'active',
),
'order' => 'Lastviewedproduct.modified DESC',
'limit' => 5,
'recursive' => 2,
));
This is what it returns:
(int) 2 => array(
'Lastviewedproduct' => array(
'id' => '97',
'sessionid' => '14035312318244955',
'product_id' => '2'
),
'Product' => array(
'id' => '2',
'name' => 'blokhut 2',
'slug' => 'blokhut-2',
'price' => '200.00',
'Category' => array(
(int) 0 => array(
'id' => '218',
'zosomodule_id' => '25',
'user_add_id' => '0',
'user_update_id' => '0',
'created' => '2013-11-27 10:00:00',
'modified' => '2013-11-27 10:00:00',
'status' => 'active',
'visibility' => true,
'parent_id' => '2',
'shipment_id' => '1',
'name' => 'Metalen Tuinbergingen ',
'slug' => 'metalen-tuinbergingen-',
'pagetitle' => 'Metalen Tuinbergingen ',
'content' => '<p class="p1"><span class="s1">Metalen tuinbergingen zijn een praktische en kwalitatieve oplossing voor meer bergruimte in uw tuin. Wij hebben diverse merken metalen bergingen zoals </span><span class="s2">Biohort</span><span class="s1"> en </span><span class="s2">Yardmaster</span><span class="s1">. Hier hebben wij tevens ook tuinkasten en opbergboxen van.</span></p>
<p class="p1"><span class="s1"><br /></span></p>
<p class="p1"><span class="s1">Mocht u op zoek zijn naar andere varianten tuinbergingen, bekijk dan ons gehele assortiment </span><span class="s2">tuinbergingen.</span></p>',
'metatitle' => 'Metalen tuinbergingen',
'metadescription' => null,
'discountoffer' => false,
'discount_normal' => '0.00',
'discount_max' => '0.00',
'discount_ideal' => '0.00',
'cost_rembours' => '0.00',
'in_mainmenu' => true,
'hide_sidebar' => false,
'show_block' => true,
'sort_order' => '0',
'ProductsCategory' => array(
'id' => '2',
'created' => '2014-01-01 10:00:00',
'modified' => '2014-01-01 10:00:00',
'product_id' => '2',
'category_id' => '218'
)
)
),
'Mediafile' => array()
)
),
I don't want all these Category fields, but only Category.id, Category.name and Category.slug for example. How do i say that in my find? As you can see I tried adding Category.id in my fields array, but then I get an error. I read something about containable, but I didn't get that working and I don't know if that is the solution.
Thanks in advance for your help!
You can use ContainableBehavior for this task. You should write something like this:
1 attach behavior
permanent
class Lastviewedproduct extends AppModel {
public $actsAs = array('Containable');
....
}
on demand
$this->Lastviewedproduct->Behaviors->load('Containable');
2 specify fields
$this->Lastviewedproduct->find('all', array(
'contain' => array(
'Product' => array(
'Category' => array(
'fields' => array('id','name','slug')
),
),
'Mediafile'
)
));
Add field parameter with relation ship where you give relationship to Category MODEL
public $hasMany = array(
'Category' => array(
'className' => 'Category',
'fields' => array('Category.id','Category.name','Category.slug')
)
);
As Dimitry said above - Containable is the way to go with this:
In your Models/AppModel.php (Create it if it doesn't exist) change the call to have the following:
class AppModel extends Model {
public $actsAs = array('Containable');
}
Then you would do as Dimitry said above - this will allow you to only return the fields you want.
'recursive' => 2
will eventually cause you so much slowness that your app will be painful to use, most people set recursive = 1 in their AppModel and use containable the rest of the time.
So, I have this method where I save a model and its associated models. It has validation rules, but not on the fields that are causing the errors.
The data being passed to the save method is ok, the return of the save method using atomic = false is true on every field except two, that do not have any validation.
The saveAll/saveAssociated fails, and the return of $this->Model->validationErrors is the list of the fields that are not being validated as a empty array.
I already had searched for this on web and found a similar problem to mine here on StackOverflow, but the answer didn't solve my problem.
EDIT
The link to the problem similar to mine: CakePHP save failing with validation errors set but empty
EDIT 2
Here's the code. The rest of the models are ok, so I won't post it here.
**Add Method**
public function add() {
if ($this->request->is('post')) {
if(empty($this->request->data['User']['referer'])) {
$this->request->data['Partner']['status'] = 1;
$this->request->data['User']['group_id'] = 2;
$this->request->data['User']['status'] = 1;
$this->request->data['Customer']['birthday'] = implode("-", array_reverse(explode("/", $this->request->data['Customer']['birthday'])));
$this->request->data['Customer']['newsletter'] = ($this->request->data['Customer']['newsletter'] == "1") ? true : false;
$this->request->data['Customer']['accept'] = ($this->request->data['Customer']['accept'] == "1") ? true : false;
if(!$this->request->data['Customer']['accept']) {
$this->Session->setFlash(__('You must accept the terms and conditions to proceed.'), 'danger');
} else {
$this->Customer->Partner->create();
var_dump($this->Customer->Partner->saveAssociated($this->request->data, array('atomic' => false)));
debug($this->Customer->Partner->validationErrors);
exit();
if($this->Partner->saveAll($this->request->data)) {
$this->Session->setFlash(__('Your profile has been saved. Welcome to Abaixo da Tabela.'), 'success');
$this->redirect(array('controller' => 'pages', 'action' => 'display', 'home', 'admin' => false));
} else {
$this->Session->setFlash('Ocorreu um erro ao tentar salvar seu perfil. Verifique se os campos estão preenchidos corretamente e tenta novamente.', 'danger');
}
}
}
}
}
** Partner Model **
class Partner extends AppModel {
public $validate = array(
'person_type' => array(
'notEmpty' => array(
'rule' => array('notEmpty'),
),
),
'status' => array(
'numeric' => array(
'rule' => array('numeric'),
),
),
);
public $hasMany = array(
'Address' => array(
'className' => 'Address',
'foreignKey' => 'partner_id',
),
'Contact' => array(
'className' => 'Contact',
'foreignKey' => 'partner_id',
),
'Customer' => array(
'className' => 'Customer',
'foreignKey' => 'partner_id',
),
'User' => array(
'className' => 'User',
'foreignKey' => 'partner_id',
),
);
}
** Address Model **
class Address extends AppModel {
public $validate = array(
'partner_id' => array(
'numeric' => array(
'rule' => array('numeric'),
),
),
'address' => array(
'notEmpty' => array(
'rule' => array('notEmpty'),
),
),
'number' => array(
'notEmpty' => array(
'rule' => array('notEmpty'),
),
),
'zip' => array(
'notEmpty' => array(
'rule' => array('notEmpty'),
),
),
'district' => array(
'notEmpty' => array(
'rule' => array('notEmpty'),
),
),
'city_id' => array(
'numeric' => array(
'rule' => array('numeric'),
),
),
'uf' => array(
'notEmpty' => array(
'rule' => array('notEmpty'),
),
),
);
public $belongsTo = array(
'Partner' => array(
'className' => 'Partner',
'foreignKey' => 'partner_id',
),
'City' => array(
'className' => 'City',
'foreignKey' => 'city_id',
)
);
}
** Customer Model **
class Customer extends AppModel {
public $validate = array(
'partner_id' => array(
'numeric' => array(
'rule' => array('numeric'),
),
),
'birthday' => array(
'date' => array(
'rule' => array('date'),
),
),
);
public $belongsTo = array(
'Partner' => array(
'className' => 'Partner',
'foreignKey' => 'partner_id',
),
'City' => array(
'className' => 'City',
'foreignKey' => 'city_id',
)
);
}
** debug($this->Customer->Partner->validationErrors) **
array (size=5)
'Partner' => boolean true
'Address' =>
array (size=7)
'zip' => boolean true
'address' => boolean true
'number' => boolean true
'adjunct' => boolean false
'district' => boolean true
'uf' => boolean true
'city_id' => boolean true
'Contact' =>
array (size=3)
'email' => boolean true
'phone_1' => boolean true
'phone_2' => boolean true
'User' =>
array (size=5)
'username' => boolean true
'password' => boolean true
'password_confirm' => boolean true
'group_id' => boolean true
'status' => boolean true
'Customer' =>
array (size=4)
'genre' => boolean true
'birthday' => boolean true
'newsletter' => boolean false
'accept' => boolean true
Note the two false responses in the return of the save method.
(Answered by a question edit. Converted to a community wiki answer. See Question with no answers, but issue solved in the comments (or extended in chat) )
The OP wrote:
Turned out that the error was in the view. The form fields for the associated models were like:
echo $this->Form->input('Customer.birthday')
echo $this->Form->input('Customer.newsletter')
echo $this->Form->input('Customer.accept')
When the correct way was:
echo $this->Form->input('Customer.0.birthday')
echo $this->Form->input('Customer.0.newsletter')
echo $this->Form->input('Customer.0.accept')
As in the Book, the fields for models with the hasMany association need a index, in that case, the 0.
I have 2 tables, Militia and Injunctions. Militia can have many injunctions and injunctions only belong to one militia. I set those up in the models but when i call a find all for militia the injunctions aren't pulled out.
Militia model
class Militia extends AppModel {
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id'
)
);
public $hasMany = array(
'Injunction' => array(
'className' => 'Injunction',
'foreignKey' => 'militia_id'
//'conditions' => array("not" => array('Injunction.removed' => null))
)
);
}
Injunctions model
class Injunction extends AppModel {
public $belongsTo = array(
'Militia' => array(
'className' => 'Militia',
'foreignKey' => 'militia_id'
)
);
}
and the query getting the militia members
$user = $this->Session->read("User");
$militias = $this->Militia->find('all',
array(
'conditions'=>array(
"Militia.user_id" => $user['User']['id'],
"Militia.deleted" => 0
)
)
);
output
/app/Controller/UsersController.php (line 41)
array(
(int) 0 => array(
'Militia' => array(
'id' => '26',
'first_name' => 'Chris',
'last_name' => 'Morris',
'created' => '2013-02-11 13:45:24',
'user_id' => '2',
'status' => '1',
'deleted' => '0'
)
),
(int) 1 => array(
'Militia' => array(
'id' => '31',
'first_name' => 'John',
'last_name' => 'Smith',
'created' => '2013-02-11 14:03:50',
'user_id' => '2',
'status' => '0',
'deleted' => '0'
)
),
(int) 2 => array(
'Militia' => array(
'id' => '32',
'first_name' => 'test',
'last_name' => 'user',
'created' => '2013-02-11 14:21:38',
'user_id' => '2',
'status' => '0',
'deleted' => '0'
)
),
(int) 3 => array(
'Militia' => array(
'id' => '33',
'first_name' => 'test',
'last_name' => 'user',
'created' => '2013-02-11 14:24:02',
'user_id' => '2',
'status' => '1',
'deleted' => '0'
)
)
)
I've done the same thing before on other projects but for some reason this one time it's not pulling out the associated data. It's probably something stupid like a typo but I've been looking and testing and can't find anything wrong.
Check ORM (hasOne, hasMany, belongsTo, hasAndBelongsToMany) definition,
Check if foreign key ids are set right
Check if there are data's in the table and its related table.
Check the results of find without condition
Check to see if recursive level is greater than 0
To rule out your old cached query issue, set debug level higher to 0
Look out in the bottom section of sql dump to see what query is being fired, if it feels ok try same query with SQL away from Cake/Php.
Hie guys i need help with my code. I have a form where a student selects subjects, marks and grades they obtained. Subjects and grades are a dropdown menu and they are in a loop. I want a student to enter at least five subjects including english language. each subject is referenced by a subject code. Can you help me do this?
My Controller function is
subject_code' => $this->data['ApplicantOlevelQualification']['subject_code'][$i],
'grade' => $this->data['ApplicantOlevelQualification']['grade'][$i],
My model is as follows
public $validate = array(
'grade' => array(
'notempty' => array(
'rule' => array('notempty'),
// extra keys like on, required, etc. go here...
),
'ruleName2' => array(
'rule' => array('inList', array('A', 'B','C','D','E')),
'message' => 'Not in range.',
),
),
'subject_code' => array(
'numeric' => array(
'rule' => array('numeric'),
//'message' => 'Your custom message here',
//'allowEmpty' => false,
//'required' => false,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
),
You need to create a custom validation.
Have a look here:
http://book.cakephp.org/1.3/view/1179/Custom-Validation-Rules
An example:
<?php
class User extends AppModel {
var $name = 'User';
var $validate = array(
'promotion_code' => array(
'rule' => array('limitDuplicates', 25),
'message' => 'This code has been used too many times.'
)
);
function limitDuplicates($check, $limit){
//$check will have value: array('promomotion_code' => 'some-value')
//$limit will have value: 25
$existing_promo_count = $this->find( 'count', array('conditions' => $check, 'recursive' => -1) );
return $existing_promo_count < $limit;
}
}
?>
a post has a user (belongsTo) and a user has a profile (hasOne). Normally everything works: I can access from Post to User and from User to Profile.
From Post, all works using a generic Find. The result is more or less this (I deleted some keys, just for example):
array(
'Post' => array(
'id' => '1',
'category_id' => '1',
'user_id' => '1',
'title' => 'Example',
'text' => '',
),
'User' => array(
'password' => '*****',
'id' => '1',
'group_id' => '1',
'username' => 'mirko',
'status' => 'active',
'Profile' => array(
'id' => '1',
'user_id' => '1',
'first_name' => 'Mirko',
'last_name' => 'Pagliai',
),
)
)
The problem comes when I use "fields", when I want to extract only a few fields.
For example, this works:
'fields' => array('id', 'title', 'User.id')
and the result is:
array(
'Post' => array(
'id' => '1',
'title' => 'Articolo di prova :-)'
),
'User' => array(
'id' => '1'
)
)
But I don't understand, always using "fields", how to access Profile.
These don'yt work:
'fields' => array('id', 'title', 'User.id', 'Profile.id')
'fields' => array('id', 'title', 'User.id', 'User.Profile.id')
I always get this error "Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'User.Profile.id' in 'field list'".
What's wrong? Thanks.
EDIT:
I'm not using the Containable behavior. Maybe is this the problem? Is it essential in this case?
An example, using paginate:
$this->paginate = array(
'conditions' => $conditions,
'fields' => array('id', 'title', 'User.id', 'Profile.id'),
'limit' => 10,
'recursive' => 2,
);
EDIT2:
thanks to #Hoff, I am close to the solution. But there is still something wrong (and Containable is very useful!).
In AppModel.php I added:
...
class AppModel extends Model {
public $actsAs = array('Containable');
public $recursive = -1;
...
But this doesn't work:
$this->paginate = array(
'conditions' => $conditions,
'contain' => array(
'User' => array(
'fields' => array('id'),
'Profile' => array(
'fields' => array('id', 'full_name'),
),
),
'Category' => array(
'fields' => 'title',
),
),
'fields' => array('id', 'user_id', 'category_id', 'title', 'created', 'modified', 'published'),
'limit' => 10,
);
Cause I get:
array(
(int) 0 => array(
'Post' => array(
'id' => '1',
'user_id' => '1',
'category_id' => '1',
'title' => 'Articolo di prova :-)',
'created' => '2012-06-21 18:46:00',
'modified' => '0000-00-00 00:00:00',
'published' => true
),
'Category' => array(
'title' => 'prova',
'id' => '1'
),
'User' => array(
'id' => '1'
)
)
)
but if I add any field to User (email, username, password, etc.), this works:
'User' => array(
'fields' => array('id', 'username'),
'Profile' => array(
'fields' => array('id', 'full_name'),
),
),
And I get:
array(
(int) 0 => array(
'Post' => array(
'id' => '1',
'user_id' => '1',
'category_id' => '1',
'title' => 'Articolo di prova :-)',
'created' => '2012-06-21 18:46:00',
'modified' => '0000-00-00 00:00:00',
'published' => true
),
'Category' => array(
'title' => 'prova',
'id' => '1'
),
'User' => array(
'id' => '1',
'username' => 'mirko',
'Profile' => array(
'id' => '1',
'full_name' => 'Mirko Pagliai'
)
)
)
)
Instead of using the 'recursive' key, I highly suggest using the containable behavior. The documentation on the behavior can be found here. I would suggest applying the behavior in your AppModel so you don't need to add it to all your models. In Model/AppModel.php:
App::uses('Model', 'Model');
class AppModel extends Model {
public $actsAs = array('Containable');
public $recursive = -1;
}
Then, for your pagination:
$this->paginate = array(
'conditions' => $conditions,
'fields' => array('id', 'title'),
'limit' => 10,
'contain' => array(
'User' => array(
'fields' => array('id'),
'Profile' => array(
'fields' => array('id')
)
)
)
);