cakephp 2.2 Using contain with Auth not working - cakephp

I have a working example of Auth set up with Users & Groups, based on the cookbook tutorial. I have an additional Locations table, which is associated with my Groups.
Location hasMany Group. Group belongsTo Locations. In 2.2 I think I should be able to get location data back for a User, but am not able to.
// App controller
public function beforeFilter() {
$this->Auth->authenticate = array(
'all' => array (
'scope' => array('User.status' => 1)
),
'Form' => array(
'contain' => array('Group', 'Location'),
'fields' => array('Location.name')
)
);
The above code only works if I create a direct association between User and Location. Is it possible to use contain here with a Group to Location association?

Problem of BaseAuthenticate is how it returns user info: return $result[$model];.
So when I need contains I'm using alternative component placed in app/Controller/Auth:
App::uses('FormAuthenticate', 'Controller/Component/Auth');
class FormAndContainableAuthenticate extends FormAuthenticate {
protected function _findUser($username, $password) {
if (empty($this->settings['contain'])) { //< deafult
$userData = parent::_findUser($username, $password);
} else { //< with contains
$userModel = $this->settings['userModel'];
list($plugin, $model) = pluginSplit($userModel);
$fields = $this->settings['fields'];
$conditions = array(
$model . '.' . $fields['username'] => $username,
$model . '.' . $fields['password'] => $this->_password($password),
);
if (!empty($this->settings['scope'])) {
$conditions = array_merge($conditions, $this->settings['scope']);
}
$modelObj = ClassRegistry::init($userModel);
$modelObj->contain($this->settings['contain']);
$result = $modelObj->find('first', array(
'conditions' => $conditions
));
if (empty($result) || empty($result[$model])) {
return false;
}
foreach($result as $modelName => $modelData) {
if ($modelName !== $model) {
$result[$model][$modelName] = $modelData;
}
}
$userData = $result[$model];
}
// remove dangerous fields like password
unset($userData[$this->settings['fields']['password']]);
if (!empty($this->settings['exclude'])) {
foreach ($this->settings['exclude'] as $fieldName) {
unset($userData[$fieldName]);
}
}
return $userData;
}
}
As you can see - it uses parent Component when no contains provided.
Also some bonus: you can provide a set of fields to remove from resulting array. Just pass field names via 'exclude' key
How to use Component:
public $components = array(
'Auth' => array(
'authenticate' => array(
'FormAndContainable' => array(
'fields' => array(
'username' => 'username',
'password' => 'password',
),
'userModel' => 'Staff',
'contain' => array('StaffPermission'),
'exclude' => array('plain_password')
)
),
),
);

If User is not linked to Location, but Group is, you should try this:
'contain' => array('Group' => array('Location')),

Related

how to use countercache for different alias in cakephp model?

I have the following models: Attachment and PurchaseOrder hence the datatables attachments and purchase_orders.
In PurchaseOrder, I have
class PurchaseOrder extends AppModel {
public $hasMany = array(
'Pdf' => array(
'className' => 'Attachment',
'foreignKey' => 'foreign_key',
'conditions' => array(
'Pdf.model' => 'PurchaseOrder'
)
),
'Zip' => array(
'className' => 'Attachment',
'foreignKey' => 'foreign_key',
'conditions' => array(
'Zip.model' => 'PurchaseOrder'
)
),
In Attachment, I have the following:
public $belongsTo = array(
'PurchaseOrder' => array(
'className' => 'PurchaseOrder',
'foreignKey' => 'foreign_key',
'counterCache' => array(
'attachment_count' => array('Pdf.model' => 'PurchaseOrder'),
)
),
My problem is when I try to use $this->PurchaseOrder->Zip->save($data); I run into problem because the alias Pdf is not found.
How do I overcome this while maintaining the countercache behavior of updating the attachment_count inside purchase_orders?
Note that if a PurchaseOrder is associated with 3 Pdf Attachments and 2 Zip Attachments, the attachment_count should read 3.
I am using cakephp 2.4.2
I stopped using counterCache and used afterSave instead.
/**
* Called after each successful save operation.
*
* #param boolean $created True if this save created a new record
* #param array $options Options passed from Model::save().
* #return void
* #link http://book.cakephp.org/2.0/en/models/callback-methods.html#aftersave
* #see Model::save()
*/
public function afterSave($created, $options = array()) {
// update the PurchaseOrder based on pdf
if (isset($this->data['Pdf'])) {
$this->updatePurchaseOrderAttachmentCount($this->data);
}
}
/**
*
* #param Array $data where we expect key Pdf
*/
public function updatePurchaseOrderAttachmentCount($data) {
// check for Pdf
$pdfSet = isset($data['Pdf']);
if (!$pdfSet) {
throw new Exception('we expect Pdf as a key in your $data');
}
// check for foreign_key and model
$pdfFKSet = isset($data['Pdf']['foreign_key']);
$pdfModelSet = isset($data['Pdf']['model']);
if (!$pdfFKSet || !$pdfModelSet) {
throw new Exception('we expect foreign_key and model as keys in your $data["Pdf"]');
}
$count = $this->find('count', array(
'conditions' => array(
'model' => 'PurchaseOrder',
'type' => 'application/pdf',
'foreign_key' => $data['Pdf']['foreign_key'],
)
));
if (!is_numeric($count)) {
throw new Exception('we expect numeric in the $count');
}
$poData = array(
'PurchaseOrder' => array(
'id' => $data['Pdf']['foreign_key'],
'attachment_count' => $count,
)
);
return $this->PurchaseOrder->save($poData);
}
I know this post is very old, but I recently faced a similar issue and found another way to handle the same problem. You can continue using the counterCache approach with multiple counterCache, the only difference would be, you need to add you association on the fly, in the __construct method of your model.
public function __construct($id = false, $table = null, $ds = null) {
parent::__construct($id, $table, $ds);
/**
* When using Aliases, associations need to be added on the fly, otherwise, the CounterScope conditions would result in an SQL Error, during counterCache updates
*/
$this->bindModel(
array('belongsTo' => array(
'PurchaseOrder' => array(
'className' => 'PurchaseOrder',
'foreignKey' => 'foreign_key',
'counterCache' => array(
'attachment_count' => array($this->alias . '.model' => 'PurchaseOrder'),
'attachment_count' => array($this->alias . '.model' => 'PurchaseOrder')
)
)
)
)
);
}

Simple CakePHP search action

I want a simple search feature that can search the current selected results on the model's index page. I have created a model Search which has no actual table:
class Search extends AppModel {
protected $_schema = array(
'search_term' => array('type' => 'string' , 'null' => true, 'default' => '', 'length' => '255'),
'model' => array('type' => 'string' , 'null' => true, 'default' => '', 'length' => '255'),
);
public $useTable = false;
public $validate = array(
'search_term' => array(
'notEmpty' => array(
'rule' => array('notEmpty'),
'message' => 'Please enter a search term'
),
'between' => array(
'rule' => array('between',3,30),
'message' => 'Please enter a search term greater than 3 characters.'
)
)
);
}
In any index.ctp view I have this with a hidden input field with the model's name:
echo $this->Form->create('Search, array('action' => 'search'));
echo $this->Form->input('search_term', array('label'=> 'Search'));
echo $this->Form->input('model', array('type'=> 'hidden', 'value'=>$this->params['controller']));
echo $this->Form->end(__('Submit'));
In the SearchesController:
public function search() {
$conditions = null;
if( $this->request->is('post') ) {
$searchModel = $this->request->data[$this->modelClass]['model'];
...
$this->{$this->modelClass}->useTable = Inflector::tableize($searchModel);
...
$this->paginate = array('conditions'=>array($groups,'OR' => $conditions));
$this->set($searchModel, $this->paginate());
$this->render("/$searchModel/index");
}
Problem is paginate is returning an array with the model labelled as 'Search' (understandably because of the useTable call) and not say Groups or Users, the model's being searched. Any way to relabel the array returned from paginate to the model being searched ? The alternative is to modify all the index.ctp files or create a results.ctp for each model.
I wouldn’t create another model merely for searching; it’s a hack and not extendable.
In the past, I’ve just used parameters (usually in the query string) to alter the conditions array (whether it’s a normal find operation of a paginate operation). An example:
<?php
class ItemsController extends AppController {
public function index() {
$conditions = array();
if (isset($this->request->query['search'])) {
$conditions['Item.title'] = $this->request->query['search'];
}
$items = $this->Item->find('all', array(
'conditions' => $conditions
));
$this->set(compact('items'));
}
}
Hopefully the above demonstrates this approach.

CakePHP: Keep the same checkboxes checked after submit

How can I keep the same checkboxes checked after submit? All the other input fields on the form automatically keeps the values. I thought this would also go for checkboxes, but nope.
echo $this->Form->input('type_id', array(
'multiple' => 'checkbox',
'options' => array(
'1' => 'Til salgs',
'2' => 'Ønskes kjøpt',
'3' => 'Gis bort'
),
'div' => false,
'label' => false
));
I believe this can be done in the controller, but how?
Edit:
Since I posted this question I've changed to CakeDcs Search plugin, because I've gotten this to work with that before. Still... I can't get it to work this time.
Adding model and controller code:
AppController
public $components = array('DebugKit.Toolbar',
'Session',
'Auth' => array(
'loginAction' => '/',
'loginRedirect' => '/login',
'logoutRedirect' => '/',
'authError' => 'Du må logge inn for å vise denne siden.',
'authorize' => array('Controller'),
),
'Search.Prg'
);
public $presetVars = true; //Same as in model filterArgs(). For Search-plugin.
AdsController
public function view() {
$this->set('title_for_layout', 'Localtrade Norway');
$this->set('show_searchbar', true); //Shows searchbar div in view
$this->log($this->request->data, 'debug');
//Setting users home commune as default filter when the form is not submitted.
$default_filter = array(
'Ad.commune_id' => $this->Auth->user('User.commune_id')
);
$this->Prg->commonProcess(); //Search-plugin
$this->paginate = array(
'conditions' => array_merge($default_filter, $this->Ad->parseCriteria($this->passedArgs)), //If Ad.commune_id is empty in second array, then the first will be used.
'fields' => $this->Ad->setFields(),
'limit' => 3
);
$this->set('res', $this->paginate());
}
Model
public $actsAs = array('Search.Searchable');
public $filterArgs = array(
'search_field' => array('type' => 'query', 'method' => 'filterSearchField'),
'commune_id' => array('type' => 'value'),
'type_id' => array('type' => 'int')
);
public function filterSearchField($data) {
if (empty($data['search_field'])) {
return array();
}
$str_filter = '%' . $data['search_field'] . '%';
return array(
'OR' => array(
$this->alias . '.title LIKE' => $str_filter,
$this->alias . '.description LIKE' => $str_filter,
)
);
}
/**
* Sets the fields which will be returned by the search.
*
* #access public
* #return array Database table fields
* #author Morten Flydahl
*
*/
public function setFields() {
return array(
'Ad.id',
'Ad.title',
'Ad.description',
'Ad.price',
'Ad.modified',
'User.id',
'User.first_name',
'User.middle_name',
'User.last_name',
'User.link',
'User.picture_url',
'Commune.name',
'Type.id',
'Type.name'
);
}
You have to set manually the selected option of the input, as an array with "keys = values = intval(checkbox id)"
I cannot explain why this format, but this is the only way I get it to work.
Here is my code:
echo $this->Form->create('User');
// Read the submitted value
$selected = $this->Form->value('User.Albums');
// Formats the value
if (empty($selected)) {
$selected = array(); // avoid mess
} else {
$selected = array_map('intval', $selected);
$selected = array_combine ($selected, $selected);
}
// Renders the checkboxes
echo $this->Form->input('Albums',array(
'type' => 'select',
'multiple' => 'checkbox',
'options' => $albums, // array ( (int)id => string(label), ... )
'selected' => $selected, // array ( (int)id => (int)id, ... )
));
Hope this helps.
++

cakephp beforeSave callback in Behaviors don't save data correctly

Controller:
$data =
array(
'ContentI18n' =>
array(
0 =>
array(
'title' => 'first',
'author' => 'first',
'source' => 'sgfsdfrst',
'lang' => 'fa',
),
),
'Content' =>
array(
'publish' => 1,
'type' => 3,
'pages' => 8,
'volume' => 7,
'modified' => '2012-05-27 14:16:37',
'created' => '2012-05-27 14:16:37',
'lang' => 'fa',
),
);
$this->Content->create();
$this->Content->saveAll($data);
Model:
public $hasMany = array(
'ContentI18n' => array(
'className' => 'ContentI18n',
)
);
beforeSave function in behavior:
public function beforeSave(Model $model) {
// Define the new Translate model
App::uses($model->name . 'I18n', 'Model');
$varI18n = $model->name . 'I18n';
$modelI18n = new $varI18n;
foreach ($model->data[$model->name] as $key => $data) {
if (!in_array($key, $this->setting))
$modelData[$model->name][$key] = $data;
else
$modelData[$model->name . 'I18n'][0][$key] = $data;
}
$modelData[$model->name . 'I18n'][0]['lang'] = $model->locale;
$modelData[$model->name]['lang'] = $model->locale;
$model->data = $modelData;
//pr($model->data);
return TRUE;
}
every things seems be cause when is save it directly it's save with saveAll. but when i use same structure of data in behavior did not work without any error.
I found out that beforeSave callback will not trigger by saveAll. for executing some code before saveAll we must override saveAll function in our model.
public function saveAll($data, $options = array()) {
/*
your code you want execute before saving...
*/
parent::saveAll($data, $options);
}
for other saving methods such as saveMany,saveAssociated,... beforeSave trigger by them.

CakePHP - How can I find all languages with tongue twisters?

Model
<?php
class Tonguetwister extends AppModel {
var $name = 'Tonguetwister';
//The Associations below have been created with all possible keys, those that are not needed can be removed
var $belongsTo = array(
'language' => array(
'className' => 'language',
'foreignKey' => 'language_alias',
'dependent'=> true
)
);
}
?>
Controller
<?php
class TonguetwistersController extends AppController {
var $name = 'Tonguetwisters';
var $uses = array('Tonguetwister', 'Language');
function index() {
$this->set('languages', $this->Language->find('all'));
}
function view($id = null) {
if (!$id) {
$this->Session->setFlash(__('Invalid tonguetwister', true));
$this->redirect(array('action' => 'index'));
}
$this->set('tonguetwisters', $this->Tonguetwister->find('all', array('conditions' => array('language_alias' => $id))));
}
}
?>
I only want to see languages on index() that have tongue twisters. How can I do this?
There might be a more efficient way, but here's how to pick only unique languages from the Tonguetwister table:
function index() {
$languageList = $this->Tonguetwister->find(
'list',
array(
'fields' => array( 'language_alias', 'language_alias' ),
'group' => 'Tonguetwister.language_alias',
'recursive' => -1
)
);
// $languageList is now an array that holds the language ids
$this->set(
'languages',
$this->Tonguetwister->Language->find(
'all',
array(
'conditions' => array(
'Language.id' => $languageList
)
)
)
);
}
By the way, you don't need to put Language into $uses. Since they have a relation set you can access the Language model with $this->Tonguetwister->Language.
You don't really need to do two SQL queries for this. If the tables are joined on "language_alias" you can do something like this:
function index() {
$this->Language->recursive = 0;
$this->set('languages', $this->Language->find('all', array(
'conditions' => array($this->Language->alias.'.language_alias' => $this->Tonguetwister->alias.'.language_alias')
));
}
You should just do one query that's going to join the tables properly.

Resources