<?php
/**
* OrderedBehavior
*
* @developer Alexander Morland ( aka. alkemann)
* @license MIT
* @version 2.1
* @modified 27. august 2008
*
* This behavior lets you order items in a very similar way to the tree
* behavior, only there is only 1 level. You can however have many
* independent lists in one table. Usually you use a foreign key to
* set / see what list you are in (see example bellow) or if you have
* just one list for the entire table, you can do that too.
*
* What it does:
*
* It manages the creation and updating of the order field. It
* also sets the models order property to this field. When adding new
* nodes or deleting old ones, this behavior will do the necisary changes
* to keep the list working properly. It is build to be completely
* automagic after the initial configuration by letting it know
* your foreign_key and weight fields.
*
* Usage example :
*
* Lets say you have books with pages and want the pages ordered
* by page number (obviously a book sorted alphabetically would be
* silly). So you have these models:
*
* Book hasMany Page
* Page belongsTo Book
*
* The Page model has fields :
*
* id
* content
* book_id
* page_number
*
* To set up this behavior we add this property to the Page model :
*
* public $actsAs = array('Ordered' => array(
* 'field' => 'page_number',
* 'foreign_key' => 'book_id'
* ));
*
* Now when you save a new page (no changes needed to action or view,
* but leave page_number out of the form), it will be added to the end
* of the book.
*
* When deleting, the weights will automatically be adjusted to fill in
* the vacum.
*
* NB! Note that if using Model::deleteAll() it is VERY important that you
* assign it to use callbacks 'beforeDelete' and 'afterDelete', like this:
*
* // in controller action
* $this->Page->deleteAll(array('user_id'=>22),true,array('beforeDelete','afterDelete'));
*
* Now lets say the last two pages to be created got made in the wrong
* order, so you want to move the last page "up" one space. With the
* a simple controller call to the model like this that can be achieved:
*
* // in a controller action :
* $this->Page->moveUp($id);
* // the id here is the id of the newest page
*
* You find that the first page you made is suppose to be the 5 pages later:
*
* // in a controller action :
* $this->Page->moveDown($id, 5);
*
* Also you discovered that in the first page got put in the middle. This
* can easily be moved first by doing this :
*
* // in a controller action :
* $this->Page->moveUp($id,true);
* // true will move it to the extre in that direction
*
* You can also use actions to find out if the node is first or last page :
*
* - isFirst($id)
* - isLast($id)
*
* And a last feature is the ability to sort the list by any field
* you want and have it set weights based on that. You do that like this :
*
* //in controller action :
* $this->Page->sortby('content DESC', $book_id);
* // dont ask me why you would sort the pages of a book by its content lol
*
* Note that this behaviour will also let you sort an entire table as one list.
* To do that you simply set the 'foreign_key' to false (and dont create the field
* in the table). Now there will only be one set of weights. (Note you need the weight
* field as normal)
*
* @author Alexander Morland aka alkemann
* @license MIT
* @modified 17. nov. 2008 (model independent settings)
* @version 2.1.3
*
*/
class OrderedBehavior extends ModelBehavior {
public $name = 'Ordered';
/**
* field : (string) The field to be ordered by.
*
* foreign_key : (string) The field to identify one SET by.
* Each set has their own order (ie they start at 1).
* Set to FALSE to not use this feature (and use only 1 set)
*/
protected $_defaults = array('field' => 'weight', 'foreign_key' => 'order_id');
public function setup(Model $Model, $config = array()) {
if (!is_array($config)) {
$config = array();
}
$this->settings[$Model->alias] = array_merge($this->_defaults, $config);
$Model->order = $Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' ASC';
}
public function beforeDelete(Model $Model, $cascade = true) {
$Model->read();
$highest = $this->_highest($Model);
if (!empty($Model->data) && ($Model->data[$Model->alias][$Model->primaryKey] == $highest[$Model->alias][$Model->primaryKey])) {
$Model->data = null;
}
return true;
}
public function afterDelete(Model $Model) {
if ($Model->data) {
// What was the weight of the deleted model?
$oldWeight = $Model->data[$Model->alias][$this->settings[$Model->alias]['field']];
// update the weight of all models of higher weight by
$action = array($this->settings[$Model->alias]['field'] => $this->settings[$Model->alias]['field'] . ' - 1');
$conditions = array(
$Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' >' => $oldWeight);
if ($this->settings[$Model->alias]['foreign_key']) {
$conditions[$Model->alias . '.' . $this->settings[$Model->alias]['foreign_key']] = $Model->data[$Model->alias][$this->settings[$Model->alias]['foreign_key']];
}
// decreasing them by 1
return $Model->updateAll($action, $conditions);
}
return true;
}
/**
* Sets the weight for new items so they end up at end
*
* @todo add new model with weight. clean up after
* @param Model $Model
*/
public function beforeSave(Model $Model) {
// Check if weight id is set. If not add to end, if set update all
// rows from ID and up
if (!isset($Model->data[$Model->alias][$Model->primaryKey])) {
// get highest current row
$highest = $this->_highest($Model);
// set new weight to model as last by using current highest one + 1
$Model->data[$Model->alias][$this->settings[$Model->alias]['field']] = $highest[$Model->alias][$this->settings[$Model->alias]['field']] + 1;
}
return true;
}
/**
* Moving a node to specific weight, it will shift the rest of the table to make room.
*
* @param Object $Model
* @param int $id The id of the node to move
* @param int $newWeight the new weight of the node
* @return boolean True of move successful
*/
public function moveTo(Model $Model, $id = null, $newWeight = null) {
if (!$id || !$newWeight || $newWeight < 1) {
return false;
}
$highest = $this->_highest($Model);
// fetch the model and its old weight
$oldWeight = $this->_read($Model, $id);
//check if new weight is too big
if ($newWeight > $highest[$Model->alias][$this->settings[$Model->alias]['field']]) {
return false;
}
if ($newWeight === true && $oldWeight == 0) {
$newWeight = $highest[$Model->alias][$this->settings[$Model->alias]['field']] + 1;
}
if (empty($Model->data)) {
return false;
}
$conditions = array();
if ($this->settings[$Model->alias]['foreign_key']) {
$conditions[$Model->alias . '.' . $this->settings[$Model->alias]['foreign_key']] = $Model->data[$Model->alias][$this->settings[$Model->alias]['foreign_key']];
}
// give Model new weight
$Model->data[$Model->alias][$this->settings[$Model->alias]['field']] = $newWeight;
if ($newWeight == $oldWeight) {
// move to same location?
return false;
} elseif ($oldWeight == 0) {
$action = array(
$Model->alias . '.' . $this->settings[$Model->alias]['field'] => $Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' + 1');
$conditions[$Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' >='] = $newWeight;
} elseif ($newWeight > $oldWeight) {
// move all nodes that have weight > old_weight AND <= new_weight up one (-1)
$action = array(
$Model->alias . '.' . $this->settings[$Model->alias]['field'] => $Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' - 1');
$conditions[$Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' <='] = $newWeight;
$conditions[$Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' >'] = $oldWeight;
} else { // $newWeight < $oldWeight
// move all where weight >= new_weight AND < old_weight down one (+1)
$action = array(
$Model->alias . '.' . $this->settings[$Model->alias]['field'] => $Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' + 1');
$conditions[$Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' >='] = $newWeight;
$conditions[$Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' <'] = $oldWeight;
}
$Model->updateAll($action, $conditions);
return $Model->save(null, false);
}
/**
* Take in an order array and sorts the list based on that order specification
* and creates new weights for it. If no foreign key is supplied, all lists
* will be sorted.
*
* @todo foreign key independent
* @param Object $Model
* @param array $order
* @param mixed $foreignKey
* $returns boolean true if successfull
*/
public function sortBy(Model $Model, $order, $foreignKey = null) {
$fields = array($Model->primaryKey, $this->settings[$Model->alias]['field']);
$conditions = array(1 => 1);
if ($this->settings[$Model->alias]['foreign_key']) {
if (!$foreignKey) {
return false;
}
$fields[] = $this->settings[$Model->alias]['foreign_key'];
$conditions = array(
$Model->alias . '.' . $this->settings[$Model->alias]['foreign_key'] => $foreignKey);
}
$all = $Model->find('all', array(
'fields' => $fields,
'conditions' => $conditions,
'recursive' => -1,
'order' => $order));
$i = 1;
foreach ($all as $key => $one) {
$all[$key][$Model->alias][$this->settings[$Model->alias]['field']] = $i++;
}
return $Model->saveAll($all);
}
/**
* Reorder the node, by moving it $number spaces up. Defaults to 1
*
* If the node is the first node (or less then $number spaces from first)
* this method will return false.
*
* @param AppModel $Model
* @param mixed $id The ID of the record to move
* @param mixed $number how many places to move the node or true to move to last position
* @return boolean true on success, false on failure
* @access public
*/
public function moveUp(Model $Model, $id = null, $number = 1) {
if (!$id) {
if ($Model->id) {
$id = $Model->id;
} elseif (!empty($Model->data) && isset($Model->data[$Model->alias][$Model->primaryKey])) {
$id = $Model->data[$Model->alias][$Model->primaryKey];
} else {
return false;
}
}
$oldWeight = $this->_read($Model, $id);
if (empty($Model->data)) {
return false;
}
if (is_numeric($number)) {
if ($number == 1) { // move 1 space
$previous = $this->_previous($Model);
if (!$previous) {
return false;
}
$Model->data[$Model->alias][$this->settings[$Model->alias]['field']] = $previous[$Model->alias][$this->settings[$Model->alias]['field']];
$previous[$Model->alias][$this->settings[$Model->alias]['field']] = $oldWeight;
$data[0] = $Model->data;
$data[1] = $previous;
return $Model->saveAll($data, array('validate' => false));
} elseif ($number < 1) { // cant move 0 or negative spaces
return false;
} else { // move Model up N spaces UP
if ($this->settings[$Model->alias]['foreign_key']) {
$conditions = array(
$Model->alias . '.' . $this->settings[$Model->alias]['foreign_key'] => $Model->data[$Model->alias][$this->settings[$Model->alias]['foreign_key']]);
} else {
$conditions = array();
}
// find the one occupying new space and its weight
$newWeight = $Model->data[$Model->alias][$this->settings[$Model->alias]['field']] - $number;
// check if new weight is possible. else move last
if (!$this->_findByWeight($Model, $newWeight)) {
return false;
}
$conditions[$Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' >='] = $newWeight;
$conditions[$Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' <'] = $oldWeight;
// increase weight of all where weight > new weight and id != Model.id
$Model->updateAll(array(
$this->settings[$Model->alias]['field'] => $Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' + 1'), $conditions);
// set Model weight to new weight and save it
$Model->data[$Model->alias][$this->settings[$Model->alias]['field']] = $newWeight;
return $Model->save(null, false);
}
} elseif (is_bool($number) && $number && $Model->data[$Model->alias][$this->settings[$Model->alias]['field']] != 1) { // move Model FIRST;
if ($this->settings[$Model->alias]['foreign_key']) {
$conditions = array(
$Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' <' => $oldWeight,
$Model->alias . '.' . $this->settings[$Model->alias]['foreign_key'] => $Model->data[$Model->alias][$this->settings[$Model->alias]['foreign_key']]);
} else {
$conditions = array(
$Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' <' => $oldWeight);
}
$Model->id = $Model->data[$Model->alias][$Model->primaryKey];
$Model->saveField($this->settings[$Model->alias]['field'], 0);
$Model->updateAll(array( // update
$Model->alias . '.' . $this->settings[$Model->alias]['field'] => $Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' + 1'), $conditions);
return true;
} else { // $number is neither a number nor a bool
return false;
}
}
/**
* This will create weights based on display field. The purpose of the method is to create
* weights for tables that existed before this behavior was added.
*
* @param Object $Model
* @return boolean success
*/
public function resetWeights(Model $Model) {
if ($this->settings[$Model->alias]['foreign_key']) {
$temp = $Model->find('all', array(
'fields' => $this->settings[$Model->alias]['foreign_key'],
'group' => $this->settings[$Model->alias]['foreign_key'],
'recursive' => -1));
$foreignKeys = Set::extract($temp, '{n}.' . $Model->alias . '.' . $this->settings[$Model->alias]['foreign_key']);
foreach ($foreignKeys as $fk) {
$all = $Model->find('all', array(
'conditions' => array($this->settings[$Model->alias]['foreign_key'] => $fk),
'fields' => array(
$Model->displayField,
$Model->primaryKey,
$this->settings[$Model->alias]['field'],
$this->settings[$Model->alias]['foreign_key']),
'order' => $Model->displayField));
$i = 1;
foreach ($all as $key => $one) {
$all[$key][$Model->alias][$this->settings[$Model->alias]['field']] = $i++;
}
if (!$Model->saveAll($all)) {
return false;
}
}
} else {
$all = $Model->find('all', array(
'fields' => array(
$Model->displayField,
$Model->primaryKey,
$this->settings[$Model->alias]['field']),
'order' => $Model->displayField));
$i = 1;
foreach ($all as $key => $one) {
$all[$key][$Model->alias][$this->settings[$Model->alias]['field']] = $i++;
}
if (!$Model->saveAll($all)) {
return false;
}
}
return true;
}
/**
* Reorder the node, by moving it $number spaces down. Defaults to 1
*
* If the node is the last node (or less then $number spaces from last)
* this method will return false.
*
* @param AppModel $Model
* @param mixed $id The ID of the record to move
* @param mixed $number how many places to move the node or true to move to last position
* @return boolean true on success, false on failure
* @access public
*/
public function moveDown(Model $Model, $id = null, $number = 1) {
if (!$id) {
if ($Model->id) {
$id = $Model->id;
} elseif (!empty($Model->data) && isset($Model->data[$Model->alias][$Model->primaryKey])) {
$id = $Model->data[$Model->alias][$Model->primaryKey];
} else {
return false;
}
}
$oldWeight = $this->_read($Model, $id);
if (empty($Model->data)) {
return false;
}
if (is_numeric($number)) {
if ($number == 1) { // move node 1 space down
$next = $this->_next($Model);
if (!$next) { // it is the last node
return false;
}
// switch the node's weight around
$Model->data[$Model->alias][$this->settings[$Model->alias]['field']] = $next[$Model->alias][$this->settings[$Model->alias]['field']];
$next[$Model->alias][$this->settings[$Model->alias]['field']] = $oldWeight;
// create an array of the two nodes and save them
$data[0] = $Model->data;
$data[1] = $next;
return $Model->saveAll($data, array('validate' => false));
} elseif ($number < 1) { // cant move 0 or negative number of spaces
return false;
} else { // move Model up N spaces DWN
if ($this->settings[$Model->alias]['foreign_key']) {
$conditions = array(
$Model->alias . '.' . $this->settings[$Model->alias]['foreign_key'] => $Model->data[$Model->alias][$this->settings[$Model->alias]['foreign_key']]);
} else {
$conditions = array();
}
// find the one occupying new space and its weight
$newWeight = $Model->data[$Model->alias][$this->settings[$Model->alias]['field']] + $number;
// check if new weight is possible. else move last
if (!$this->_findByWeight($Model, $newWeight)) {
return false;
}
// increase weight of all where weight > new weight and id != Model.id
$conditions[$Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' <='] = $newWeight;
$conditions[$Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' >'] = $oldWeight;
$Model->updateAll(array(
$this->settings[$Model->alias]['field'] => $this->settings[$Model->alias]['field'] . ' - 1'), $conditions);
// set Model weight to new weight and save it
$Model->data[$Model->alias][$this->settings[$Model->alias]['field']] = $newWeight;
return $Model->save(null, false);
}
} elseif (is_bool($number) && $number) { // move Model LAST;
if ($this->settings[$Model->alias]['foreign_key']) {
$conditions = array(
$Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' >' => $oldWeight,
$Model->alias . '.' . $this->settings[$Model->alias]['foreign_key'] => $Model->data[$Model->alias][$this->settings[$Model->alias]['foreign_key']]);
} else {
$conditions = array(
$Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' >' => $oldWeight);
}
// get highest weighted row
$highest = $this->_highest($Model);
// check of Model is allready highest
if ($highest[$Model->alias][$Model->primaryKey] == $Model->data[$Model->alias][$Model->primaryKey]) {
return false;
}
// Save models as highest +1
$Model->saveField($this->settings[$Model->alias]['field'], $highest[$Model->alias][$this->settings[$Model->alias]['field']] + 1);
// updated all by taking off 1
$Model->updateAll(array( // action
$Model->alias . '.' . $this->settings[$Model->alias]['field'] => $Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' - 1'), $conditions);
return true;
} else { // $number is neither a number nor a bool
return false;
}
}
/**
* Returns true if the specified item is the first item
*
* @param Model $Model
* @param Int $id
* @return Boolean, true if it is the first item, false if not
*/
public function isFirst(Model $Model, $id = null) {
if (!$id) {
if ($Model->id) {
$id = $Model->id;
} elseif (!empty($Model->data) && isset($Model->data[$Model->alias][$Model->primaryKey])) {
$id = $Model->id = $Model->data[$Model->alias][$Model->primaryKey];
} else {
return false;
}
} else {
$Model->id = $id;
}
$Model->read();
$first = $this->_read($Model, $id);
if ($Model->data[$Model->alias][$this->settings[$Model->alias]['field']] == 1) {
return true;
} else {
return false;
}
}
/**
* Returns true if the specified item is the last item
*
* @param Model $Model
* @param Int $id
* @return Boolean, true if it is the last item, false if not
*/
public function isLast(Model $Model, $id = null) {
if (!$id) {
if ($Model->id) {
$id = $Model->id;
} elseif (!empty($Model->data) && isset($Model->data[$Model->alias][$Model->primaryKey])) {
$id = $Model->id = $Model->data[$Model->alias][$Model->primaryKey];
} else {
return false;
}
} else {
$Model->id = $id;
}
$Model->read();
$last = $this->_highest($Model);
return ($last[$Model->alias][$Model->primaryKey] == $id);
}
/**
* Removing an item from the list means to set its field to 0 and updating the other items to be "complete"
*
* @param Model $Model
* @param int $id
* @return boolean
*/
public function removefromlist(Model $Model, $id) {
$this->_read($Model, $id);
$oldWeight = $Model->data[$Model->alias][$this->settings[$Model->alias]['field']];
$action = array(
$Model->alias . '.' . $this->settings[$Model->alias]['field'] => $Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' - 1');
$conditions = array(
$Model->alias . '.' . $this->settings[$Model->alias]['field'] . ' >' => $oldWeight);
if ($this->settings[$Model->alias]['foreign_key']) {
$conditions[$Model->alias . '.' . $this->settings[$Model->alias]['foreign_key']] = $Model->data[$Model->alias][$this->settings[$Model->alias]['foreign_key']];
}
$data = $Model->data;
$data[$Model->alias][$this->settings[$Model->alias]['field']] = 0;
if (!$Model->save($data, false)) {
return false;
}
return $Model->updateAll($action, $conditions);
}
protected function _findbyweight(Model $Model, $weight) {
$conditions = array($this->settings[$Model->alias]['field'] => $weight);
$fields = array($Model->primaryKey, $this->settings[$Model->alias]['field']);
if ($this->settings[$Model->alias]['foreign_key']) {
$conditions[$Model->alias . '.' . $this->settings[$Model->alias]['foreign_key']] = $Model->data[$Model->alias][$this->settings[$Model->alias]['foreign_key']];
$fields[] = $this->settings[$Model->alias]['foreign_key'];
}
return $Model->find('first', array(
'conditions' => $conditions,
'order' => $this->settings[$Model->alias]['field'] . ' DESC',
'fields' => $fields,
'recursive' => -1));
}
protected function _highest(Model $Model) {
$options = array(
'order' => $this->settings[$Model->alias]['field'] . ' DESC',
'fields' => array($Model->primaryKey, $this->settings[$Model->alias]['field']),
'recursive' => -1);
if ($this->settings[$Model->alias]['foreign_key']) {
if (empty($Model->data) || !isset($Model->data[$Model->alias][$this->settings[$Model->alias]['foreign_key']])) {
$this->_read($Model, $Model->id);
}
$options['conditions'] = array(
$Model->alias . '.' . $this->settings[$Model->alias]['foreign_key'] => $Model->data[$Model->alias][$this->settings[$Model->alias]['foreign_key']]);
$options['fields'][] = $this->settings[$Model->alias]['foreign_key'];
}
$tempModelId = $Model->id;
$Model->id = null;
$last = $Model->find('first', $options);
$Model->id = $tempModelId;
return $last;
}
protected function _previous(Model $Model) {
$conditions = array(
$this->settings[$Model->alias]['field'] => $Model->data[$Model->alias][$this->settings[$Model->alias]['field']] - 1);
$fields = array($Model->primaryKey, $this->settings[$Model->alias]['field']);
if ($this->settings[$Model->alias]['foreign_key']) {
$conditions[$Model->alias . '.' . $this->settings[$Model->alias]['foreign_key']] = $Model->data[$Model->alias][$this->settings[$Model->alias]['foreign_key']];
$fields[] = $this->settings[$Model->alias]['foreign_key'];
}
return $Model->find('first', array(
'conditions' => $conditions,
'order' => $this->settings[$Model->alias]['field'] . ' DESC',
'fields' => $fields,
'recursive' => -1));
}
protected function _next(Model $Model) {
$conditions = array(
$this->settings[$Model->alias]['field'] => $Model->data[$Model->alias][$this->settings[$Model->alias]['field']] + 1);
$fields = array($Model->primaryKey, $this->settings[$Model->alias]['field']);
if ($this->settings[$Model->alias]['foreign_key']) {
$conditions[$Model->alias . '.' . $this->settings[$Model->alias]['foreign_key']] = $Model->data[$Model->alias][$this->settings[$Model->alias]['foreign_key']];
$fields[] = $this->settings[$Model->alias]['foreign_key'];
}
return $Model->find('first', array(
'conditions' => $conditions,
'order' => $this->settings[$Model->alias]['field'] . ' DESC',
'fields' => $fields,
'recursive' => -1));
}
protected function _all(Model $Model) {
$options = array(
'order' => $this->settings[$Model->alias]['field'] . ' DESC',
'fields' => array($Model->primaryKey, $this->settings[$Model->alias]['field']),
'recursive' => -1);
if ($this->settings[$Model->alias]['foreign_key']) {
$options['conditions'] = array(
$this->settings[$Model->alias]['foreign_key'] => $Model->data[$Model->alias][$this->settings[$Model->alias]['foreign_key']]);
$options['fields'][] = $this->settings[$Model->alias]['foreign_key'];
}
return $Model->find('all', $options);
}
protected function _read(Model $Model, $id) {
$Model->id = $id;
$fields = array($Model->primaryKey, $this->settings[$Model->alias]['field']);
if ($this->settings[$Model->alias]['foreign_key']) {
$fields[] = $this->settings[$Model->alias]['foreign_key'];
}
$Model->data = $Model->find('first', array(
'fields' => $fields,
'conditions' => array($Model->primaryKey => $id),
'recursive' => -1));
return $Model->data[$Model->alias][$this->settings[$Model->alias]['field']];
}
}