diff --git a/app/plugins/core/views/elements/comments/add.ctp b/app/plugins/core/views/elements/comments/add.ctp
index e84b269ca..9fe376848 100644
--- a/app/plugins/core/views/elements/comments/add.ctp
+++ b/app/plugins/core/views/elements/comments/add.ctp
@@ -51,7 +51,7 @@
{
if ( $field != 'comment' )
{
- echo $this->Form->input( 'Comment'.$field );
+ echo $this->Form->input( 'Comment.'.$field );
}
else
{
diff --git a/app/plugins/filter/README.markdown b/app/plugins/filter/README.markdown
new file mode 100644
index 000000000..c00eb3dff
--- /dev/null
+++ b/app/plugins/filter/README.markdown
@@ -0,0 +1,88 @@
+Filter Paginated Indexes using the CakePHP Filter Plugin
+
+## Background
+This plugin is a fork of Jose Gonzalez's Filter component(http://github.com/josegonzalez/cakephp-filter-component), which is something of a fork of James Fairhurst's Filter Component (http://www.jamesfairhurst.co.uk/posts/view/cakephp_filter_component/), which is in turn a fork by Maciej Grajcarek (http://blog.uplevel.pl/index.php/2008/06/cakephp-12-filter-component/) which is ITSELF a fork from Nik Chankov's code at http://nik.chankov.net/2008/03/01/filtering-component-for-your-tables/ .
+
+That's a lot of forks...
+
+This also contains a view helper made by 'mcurry' (http://github.com/mcurry/cakephp-filter-component).
+
+This also uses a behavior adapted from work by 'Brenton' (http://bakery.cakephp.org/articles/view/habtm-searching) to allow for HasAndBelongsToMany and HasMany relationships.
+
+This works for all relationships.
+
+## Installation
+- Clone from github : in your plugin directory type `git clone git://github.com/JeffreyMarvin/cakephp-filter-plugin.git`
+- Add as a git submodule : in your plugin directory type `git submodule add git://github.com/JeffreyMarvin/cakephp-filter-plugin.git`
+- Download an archive from github and extract it in `/plugins/filter`
+
+## Usage
+1. Include the component in your controller (AppController or otherwise)
+ var $components = array('Filter.Filter');
+2. Use something like the following in your index
+ function index() {
+ $filterOptions = $this->Filter->filterOptions;
+ $posts = $this->paginate(null, $this->Filter->filter);
+ $this->set(compact('filterOptions', 'posts'));
+ }
+3. Setup your view correctly:
+
+-Option 1: Helper
+
+Use the helper In between the row with all the column headers and the first row of data add:
+ form('Post', array('name')) ?>
+The first parameter is the model name.
+The second parameter is an array of fields.
+If you don't want to filter a particular field pass null in that spot.
+
+-Option 2: Manually
+ create('Post', array('action' => 'index', 'id' => 'filters')); ?>
+
+
+4. Add Behavior to model (only necessary for HABTM and HasMany):
+ var $actsAs = 'Filter';
+
+At this point, everything should theoretically work.
+
+For action(s) other than index, add a line to the controller such as this:
+ $this->Filter->initialize($this, array('actions' => 'admin_index'));
+
+To set it up for redirecting to the url with filters in it (which defaults to off), add a line to the controller such as this:
+ $this->Filter->initialize($this, array('redirect' => true));
+
+To set it up to include time in the filter, add a line to the controller such as this:
+ $this->Filter->initialize($this, array('useTime' => true));
+
+These different initialize options can be combined in the array.
+
+## TODO:
+<<<<<<< HEAD
+1. Better code commenting - Done, left to help enforce the habit
+2. Support Datetime - Mostly Done
+3. Support URL redirects and parsing - Mostly Done
+4. Refactor datetime filtering for ranges
+5. Allow the action to be configurable
+6. Support jQuery Datepicker
\ No newline at end of file
diff --git a/app/plugins/filter/controllers/components/filter.php b/app/plugins/filter/controllers/components/filter.php
new file mode 100644
index 000000000..05b9459e7
--- /dev/null
+++ b/app/plugins/filter/controllers/components/filter.php
@@ -0,0 +1,348 @@
+
+ * @license http://www.opensource.org/licenses/mit-license.php The MIT License
+ * @package app
+ * @subpackage app.controller.components
+ */
+class FilterComponent extends Object {
+/**
+ * Fields which will replace the regular syntax in where i.e. field = 'value'
+ * @var array
+ */
+ var $fieldFormatting = array(
+ "string" => "LIKE '%%%s%%'",
+ "text" => "LIKE '%%%s%%'",
+ "datetime" => "LIKE '%%%s%%'"
+ );
+
+/**
+ * Paginator params sent in URL
+ * @var array
+ */
+ var $paginatorParams = array(
+ 'page',
+ 'sort',
+ 'direction'
+ );
+
+/**
+ * Url variable used in paginate helper (array('url'=>$url));
+ * @var string
+ */
+ var $url = '';
+
+/**
+ * Used to tell whether the data options have been parsed
+ * @var boolean
+ */
+ var $parsed = false;
+
+/**
+ * Used to tell whether to redirect so the url includes filter data
+ * @var boolean
+ */
+ var $redirect = false;
+
+/**
+ * Used to tell whether time should be used in the filtering
+ * @var boolean
+ */
+ var $useTime = false;
+
+// class variables
+ var $filter = array();
+ var $formOptionsDatetime = array();
+ var $filterOptions = array();
+
+/**
+ * Before any Controller action
+ *
+ * @param array settings['actions'] an array of the action(s) the filter is to be applied to,
+ * @param array settings['redirect'] is whether after filtering is completed it should redirect and put the filters in the url,
+ * @param array settings['useTime'] is whether to filter date times with date in addition to time
+ */
+ function initialize(&$controller, $settings = array()) {
+ // If no action(s) is/are specified, defaults to 'index'
+ if (!isset($settings['actions']) || empty($settings['actions'])) {
+ $actions = array('index');
+ } else {
+ $actions = $settings['actions'];
+ }
+
+ if (!isset($settings['redirect']) || empty($settings['redirect'])) {
+ $this->redirect = false;
+ } else {
+ $this->redirect = $settings['redirect'];
+ }
+
+ if (!isset($settings['useTime']) || empty($settings['useTime'])) {
+ $this->useTime = false;
+ } else {
+ $this->useTime = $settings['useTime'];
+ }
+
+ foreach ($actions as $action){
+ $this->processAction($controller, $action);
+ }
+ }
+
+ function processAction($controller, $controllerAction){
+ if ($controller->action == $controllerAction) {
+ $this->filter = $this->processFilters($controller);
+ $url = (empty($this->url)) ? '/' : $this->url;
+
+ $this->filterOptions = array('url' => array($url));
+ $this->formOptionsDatetime = array(
+ 'dateFormat' => 'DMY',
+ 'empty' => '-',
+ 'maxYear' => date("Y"),
+ 'minYear' => date("Y")-2,
+ 'type' => 'date');
+
+ if (isset($controller->data['reset']) || isset($controller->data['cancel'])) {
+ $this->filter = array();
+ $this->url = '/';
+ $this->filterOptions = array();
+ $controller->redirect("/{$controller->name}/{$controllerAction}");
+ }
+ }
+ }
+
+/**
+ * Builds up a selected datetime for the form helper
+ *
+ * @param string $fieldname
+ * @return null|string
+ */
+ function processDatetime($fieldname) {
+ $datetime = null;
+
+ if (isset($this->params['named'][$fieldname])) {
+ $exploded = explode('-', $this->params['named'][$fieldname]);
+ if (!empty($exploded)) {
+ $datetime = '';
+ foreach ($exploded as $k => $e) {
+ $datetime = (empty($e)) ? (($k == 0) ? '0000' : '00') : $e;
+ if ($k != 2) {$datetime .= '-';}
+ }
+ }
+ }
+ return $datetime;
+ }
+
+/**
+ * Function which will change controller->data array
+ *
+ * @param object $controller the class of the controller which call this component
+ * @param array $whiteList contains list of allowed filter attributes
+ * @access public
+ */
+ function processFilters($controller, $whiteList = null){
+ $controller = $this->_prepareFilter($controller);
+ $ret = array();
+
+ if (isset($controller->data)) {
+ foreach ($controller->data as $model => $fields) {
+ $modelFieldNames = array();
+ if (isset($controller->{$model})) {
+ $modelFieldNames = $controller->{$model}->getColumnTypes();
+ } else if (isset($controller->{$controller->modelClass}->belongsTo[$model]) || isset($controller->{$controller->modelClass}->hasOne[$model])) {
+ $modelFieldNames = $controller->{$controller->modelClass}->{$model}->getColumnTypes();
+ }
+ if (!empty($modelFieldNames)) {
+ foreach ($fields as $filteredFieldName => $filteredFieldData) {
+ if (is_array($filteredFieldData) && $modelFieldNames[$filteredFieldName] == 'datetime') {
+ $filteredFieldData = $this->_prepareDatetime($filteredFieldData);
+ }
+ if ($filteredFieldData != '') {
+ if (is_array($whiteList) && !in_array($filteredFieldName, $whiteList) ){
+ continue;
+ }
+ if (isset($this->fieldFormatting[$modelFieldNames[$filteredFieldName]])) {
+ // insert value into fieldFormatting
+ $tmp = sprintf($this->fieldFormatting[$modelFieldNames[$filteredFieldName]], $filteredFieldData);
+ // don't put key.fieldname as array key if a LIKE clause
+ if (substr($tmp, 0, 4) == 'LIKE') {
+ $ret[] = "{$model}.{$filteredFieldName} {$tmp}";
+ } else {
+ $ret["{$model}.{$filteredFieldName}"] = $tmp;
+ }
+ } else {
+ // build up where clause with field and value
+ $ret["{$model}.{$filteredFieldName}"] = $filteredFieldData;
+ }
+ // save the filter data for the url
+ $this->url .= "/{$model}.{$filteredFieldName}:{$filteredFieldData}";
+ }
+ }
+ } else {
+ if (isset($controller->{$controller->modelClass}->hasMany[$model])) {
+ $modelFieldNames = $controller->{$controller->modelClass}->{$model}->getColumnTypes();
+ if (!empty($modelFieldNames)) {
+ foreach ($fields as $filteredFieldName => $filteredFieldData) {
+ if (is_array($filteredFieldData) && $modelFieldNames[$filteredFieldName] == 'datetime') {
+ $filteredFieldData = $this->_prepare_datetime($filteredFieldData);
+ }
+ if ($filteredFieldData != '') {
+ if (is_array($whiteList) && !in_array($filteredFieldName, $whiteList) ){
+ continue;
+ }
+ // check if there are some fieldFormatting set
+ if (isset($this->fieldFormatting[$modelFieldNames[$filteredFieldName]])) {
+ // insert value into fieldFormatting
+ $tmp = sprintf($this->fieldFormatting[$modelFieldNames[$filteredFieldName]], $filteredFieldData);
+ // don't put key.fieldname as array key if a LIKE clause
+ if (substr($tmp, 0, 4) == 'LIKE') {
+ $ret[] = "{$model}.{$filteredFieldName} {$tmp}";
+ } else {
+ $ret["{$model}.{$filteredFieldName}"] = $tmp;
+ }
+ } else {
+ $ret["{$model}.{$filteredFieldName}"] = $filteredFieldData;
+ }
+ $this->url .= "/{$model}.{$filteredFieldName}:{$filteredFieldData}";
+ }
+ }
+ }
+ } else if (isset($controller->{$controller->modelClass}->hasAndBelongsToMany[$model])) {
+ $modelFieldNames = $controller->{$controller->modelClass}->{$model}->getColumnTypes();
+ if (!empty($modelFieldNames)) {
+ foreach ($fields as $filteredFieldName => $filteredFieldData) {
+ if (is_array($filteredFieldData) && $modelFieldNames[$filteredFieldName] == 'datetime') {
+ $filteredFieldData = $this->_prepare_datetime($filteredFieldData);
+ }
+ if ($filteredFieldData != '') {
+ // if filter is in whitelist
+ if (is_array($whiteList) && !in_array($filteredFieldName, $whiteList) ){
+ continue;
+ }
+ // check if there are some fieldFormatting set
+ if (isset($this->fieldFormatting[$modelFieldNames[$filteredFieldName]])) {
+ // insert value into fieldFormatting
+ $tmp = sprintf($this->fieldFormatting[$modelFieldNames[$filteredFieldName]], $filteredFieldData);
+ // don't put key.fieldname as array key if a LIKE clause
+ if (substr($tmp, 0, 4) == 'LIKE') {
+ $ret[] = "{$model}.{$filteredFieldName} {$tmp}";
+ } else {
+ $ret["{$model}.{$filteredFieldName}"] = $tmp;
+ }
+ } else {
+ $ret["{$model}.{$filteredFieldName}"] = $filteredFieldData;
+ }
+ $this->url .= "/{$model}.{$filteredFieldName}:{$filteredFieldData}";
+ }
+ }
+ }
+ }
+ }
+ // Unset empty model data
+ if (count($fields) == 0){
+ unset($controller->data[$model]);
+ }
+ }
+ }
+ //If redirect has been set true, and the data had not been parsed before and put into the url, does it now
+ if (!$this->parsed && $this->redirect){
+ $this->url = "/Filter.parsed:true{$this->url}";
+ $controller->redirect("/{$controller->name}/index{$this->url}/");
+ }
+ return $ret;
+ }
+
+/**
+ * function which will take care of the storing the filter data and loading after this from the Session
+ * JF: modified to not htmlencode, caused problems with dates e.g. -05-
+ *
+ * @param object $controller the class of the controller which call this component
+ */
+ function _prepareFilter($controller) {
+ $filter = array();
+ if (isset($controller->data)) {
+ foreach ($controller->data as $model => $fields) {
+ if (is_array($fields)) {
+ foreach ($fields as $key => $field) {
+ if ($field == '') {
+ unset($controller->data[$model][$key]);
+ }
+ }
+ }
+ }
+
+ App::import('Sanitize');
+ $sanitize = new Sanitize();
+ $controller->data = $sanitize->clean($controller->data, array('encode' => false));
+ $filter = $controller->data;
+ }
+
+ if (empty($filter)) {
+ $filter = $this->_checkParams($controller);
+ }
+
+ $controller->data = $filter;
+ return $controller;
+ }
+
+/**
+ * function which will take care of filters from URL
+ * JF: modified to not encode, caused problems with dates
+ *
+ * @param object $controller the class of the controller which call this component
+ */
+ function _checkParams($controller) {
+ if (empty($controller->params['named'])) {
+ $filter = array();
+ }
+
+ App::import('Sanitize');
+ $sanitize = new Sanitize();
+
+ $controller->params['named'] = $sanitize->clean($controller->params['named'], array('encode' => false));
+ if (isset($controller->params['named']['Filter.parsed'])){
+ if ($controller->params['named']['Filter.parsed']){
+ $this->parsed = true;
+ $filter = array();
+ }
+ }
+
+ foreach ($controller->params['named'] as $field => $value) {
+ if (!in_array($field, $this->paginatorParams) && $field != 'Filter.parsed') {
+ $fields = explode('.', $field);
+ if (sizeof($fields) == 1) {
+ $filter[$controller->modelClass][$field] = $value;
+ } else {
+ $filter[$fields[0]][$fields[1]] = $value;
+ }
+ }
+ }
+
+ return (!empty($filter)) ? $filter : array();
+ }
+
+/**
+ * Prepares a date array for a MySQL WHERE clause
+ *
+ * @author Jeffrey Marvin
+ * @param array $date
+ * @return string
+ */
+ function _prepareDatetime($date) {
+ if ($this->useTime){
+ return "{$date['year']}-{$date['month']}-{$date['day']}"
+ . ' ' . (($date['meridian'] == 'pm' && $date['hour'] != 12) ? $date['hour'] + 12 : $date['hour'])
+ . ':' . (($date['min'] < 10) ? "0{$date['min']}" : $date['min']);
+ } else {
+ return "{$date['year']}-{$date['month']}-{$date['day']}";
+ }
+ }
+}
+?>
\ No newline at end of file
diff --git a/app/plugins/filter/models/behaviors/filter.php b/app/plugins/filter/models/behaviors/filter.php
new file mode 100644
index 000000000..ad508190f
--- /dev/null
+++ b/app/plugins/filter/models/behaviors/filter.php
@@ -0,0 +1,134 @@
+ 0) {
+
+ $associated = $model->getAssociated();
+
+ foreach ($queryData['conditions'] AS $key => $value) {
+ if(strpos($value, 'LIKE')){
+ $tmp = explode('LIKE', $value);
+ $field = $tmp['0'];
+ $search_value = 'LIKE ' . $tmp['1'];
+ } else {
+ $field = $key;
+ $search_value = $value;
+ }
+ // Period indicates that not controller's own model
+ if (strpos($field, '.')) {
+ list($associatedModel, $column) = explode('.', $field);
+ // See if it's an association
+ if (array_key_exists($associatedModel, $associated)) {
+
+ // Do stuff based on association type, so far only HABTM
+ if ($associated[$associatedModel] == 'hasAndBelongsToMany') {
+ $assoc = $model->hasAndBelongsToMany[$associatedModel];
+ $condition = $model->{$associatedModel}->find('all',
+ array(
+ 'fields' => 'DISTINCT id',
+ 'conditions' => $field . ' ' . $search_value,
+ 'recursive' => -1,
+ 'callbacks' => false // because otherwise this `beforeFind` would be called again
+ ));
+ // So far can't find a way to nicely return a distinct/unique array using the 'list'
+ // condition in `find()`, so we use 'all', and use `Set::combine()` (which is pretty
+ // much what 'list' does anyway).
+ // Another option would've been to still use 'list', but add a 'GROUP BY'
+ // (ex: 'group' => $assoc['foreignKey']) onto the query; however, this is slower
+ // for the database (arguably, what we're doing here could make up for that, so it's
+ // really a preference thing). Maybe do some testing if it's a big issue.
+ $i = 0;
+ foreach($condition AS $k => $v){
+ foreach($v AS $w => $x){
+ foreach($x AS $y => $z){
+ $conditions[$i++] = $w . '_' . $y . '=' . $z;
+ }
+ }
+ }
+ $result = $model->{$associatedModel}->{$assoc['with']}->find('all',
+ array(
+ 'fields' => 'DISTINCT '. $assoc['foreignKey'],
+ 'conditions' => array('OR' => $conditions),
+ 'recursive' => -1,
+ 'callbacks' => false // because otherwise this `beforeFind` would be called again
+ ));
+ $key_value = '{n}.'. $model->{$associatedModel}->{$assoc['with']}->name .'.'. $assoc['foreignKey'];
+
+ $result = Set::combine($result, $key_value, $key_value);
+
+ // TODO: somehow save this because some times (ex: pagination) we do a `SELECT COUNT(*)`, followed
+ // by the actually query itself, so would be nice to avoid an extra query.
+ $ids = array_keys($result);
+ // set it in our return array
+ $ret_queryData['conditions'][$model->name .'.id'] = $ids;
+ // and unset the old one, since different id field and such
+ unset($ret_queryData['conditions'][$key]);
+ } else if ($associated[$associatedModel] == 'hasMany') {
+ $assoc = $model->hasMany[$associatedModel];
+ $condition = $model->{$associatedModel}->find('all',
+ array(
+ 'fields' => 'DISTINCT id',
+ 'conditions' => $field . ' ' . $search_value,
+ 'recursive' => -1,
+ 'callbacks' => false // because otherwise this `beforeFind` would be called again
+ ));
+ // So far can't find a way to nicely return a distinct/unique array using the 'list'
+ // condition in `find()`, so we use 'all', and use `Set::combine()` (which is pretty
+ // much what 'list' does anyway).
+ // Another option would've been to still use 'list', but add a 'GROUP BY'
+ // (ex: 'group' => $assoc['foreignKey']) onto the query; however, this is slower
+ // for the database (arguably, what we're doing here could make up for that, so it's
+ // really a preference thing). Maybe do some testing if it's a big issue.
+ $i = 0;
+ foreach($condition AS $k => $v){
+ foreach($v AS $w => $x){
+ foreach($x AS $y => $z){
+ $conditions[$i++] = $y . '=' . $z;
+ }
+ }
+ }
+ $result = $model->{$associatedModel}->find('all',
+ array(
+ 'fields' => 'DISTINCT ' . $assoc['foreignKey'],
+ 'conditions' => array('OR' => $conditions),
+ 'recursive' => -1,
+ 'callbacks' => false // because otherwise this `beforeFind` would be called again
+ ));
+ $key_value = '{n}.'. $model->{$associatedModel}->name .'.'. $assoc['foreignKey'];
+
+ $result = Set::combine($result, $key_value, $key_value);
+
+ // TODO: somehow save this because some times (ex: pagination) we do a `SELECT COUNT(*)`, followed
+ // by the actually query itself, so would be nice to avoid an extra query.
+ $ids = array_keys($result);
+ // set it in our return array
+ $ret_queryData['conditions'][$model->name .'.id'] = $ids;
+ // and unset the old one, since different id field and such
+ unset($ret_queryData['conditions'][$key]);
+ }
+ }
+ }
+ }
+ }
+
+ return $ret_queryData;
+ }
+}
+?>
\ No newline at end of file
diff --git a/app/plugins/filter/views/helpers/filter.php b/app/plugins/filter/views/helpers/filter.php
new file mode 100644
index 000000000..263fef3ae
--- /dev/null
+++ b/app/plugins/filter/views/helpers/filter.php
@@ -0,0 +1,27 @@
+';
+ $output .= $this->Form->create($model, array('action' => 'index', 'id' => 'filters'));
+
+ foreach($fields as $field) {
+ if(empty($field)) {
+ $output .= '