diff options
Diffstat (limited to 'protected/modules/cms/components/Relation.php')
| -rw-r--r-- | protected/modules/cms/components/Relation.php | 636 |
1 files changed, 636 insertions, 0 deletions
diff --git a/protected/modules/cms/components/Relation.php b/protected/modules/cms/components/Relation.php new file mode 100644 index 0000000..c0dfcd1 --- /dev/null +++ b/protected/modules/cms/components/Relation.php @@ -0,0 +1,636 @@ +<?php +/* + The Relation widget is used in forms, where the User can choose + between a selection of model elements, that this models belongs to. + + It is able to handle BELONGS_TO, HAS_ONE and MANY_MANY Relations. The Relation +type is detected automatically from the Model 'relations()' section. + + The Widget has different styles in which it can render the possible choices. + Use the 'style' option to set the appropriate style. + + The following example shows how to use Relation with a minimal config, + assuming we have a Model "Post" and "User", where one User belongs + to a Post: + + <pre> + $this->widget('application.components.Relation', array( + 'model' => 'Post', + 'relation' => 'user' + 'fields' => 'username' // show the field "username" of the parent element + )); + </pre> + + Results in a drop down list in which the user can choose between + all available Users in the Database. The shown field of the + Table "User" is "username" in this example. + + You can choose the Style of your Widget in the 'style' option. + Note that a Many_Many Relation always gets rendered as a Listbox, + since you can select multiple Elements. + + 'fields' can be an array or an string. + If you pass an array to 'fields', the Widget will display every field in + this array. If you want to show further sub-relations, separate the values + with '.', for example: 'fields' => 'array('parent.grandparent.description') + + Optional Parameters: + + You can use 'field' => 'post_userid' if the field in the model + that represents the foreign model is called different than in the + relation + + Use 'relatedPk' => 'id_of_user' if the primary Key of the Foreign + Model differs from the one given in the relation. + + Normally you shouldn´t use this fields cause the Widget get the relations + automatically from the relation. + + Use 'allowEmpty' to let the user be able to choose no parent. If you + set this to a string, this string will be displayed with the available + choices. + + With 'showAddButton' => 'false' you can disable the 'create new Foreignkey' + Button generated beside the Selectbox. + + Define the AddButtonString with 'addButtonString' => 'Add...'. This string + is set default to '+' + + When using the '+' button you most likely want to return to where you came. + To accomplish this, we pass a 'returnTo' parameter by $_GET. + The Controller can send the user back to where he came from this way: + + <pre> + if($model->save()) + if(isset($_GET['returnTo'])) + $this->redirect(array(urldecode($_GET['returnTo']))); + </pre> + + Using the 'style' option we can configure how our Widget gets rendered. + The following styles are available: + Selectbox (default), Listbox, Checkbox and in MANY_MANY relations 'twopane' + The style is case insensitive so one can use dropdownlist or dropDownList. + + Use the option 'createAction' if the action to add additional foreign Model + options differs from 'create'. + + With 'parentObjects' you can limit the Parent Elements that are being shown. + It takes an array of elements that could be returned from an scope or + an SQL Query. + + The parentObjects can be grouped, for example, with + 'groupParentsBy' => 'city' + + Use the option 'htmlOptions' to pass any html Options to the + Selectbox/Listbox form element. + + Full Example: + <pre> + $this->widget('application.components.Relation', array( + 'model' => 'Post', + 'field' => 'Userid', + 'style' => 'ListBox', + 'parentObjects' => Parentmodel::model()->findAll('userid = 17'), + 'groupParentsBy' => 'city', + 'relation' => 'user', + 'relatedPk' => 'id_of_user', + 'fields' => array( 'username', 'username.group.groupid' ), + 'delimiter' => ' -> ', // default: ' | ' + 'returnTo' => 'model/create', + 'addButtonLink' => 'othercontroller/otheraction', // default: '' + 'showAddButton' => 'click here to add a new User', // default: '' + 'htmlOptions' => array('style' => 'width: 100px;') + )); + </pre> + + + @author Herbert Maschke <thyseus@gmail.com> + @version 1.0rc5 + @since 1.1 + */ + +class Relation extends CWidget +{ + // this Variable holds an instance of the Object + protected $_model; + + // this Variable holds an instance of the related Object + protected $_relatedModel; + + // draw the relation of which model? + public $model; + + // which relation should be rendered? + public $relation; + + public $field; + + // the Primary Key of the foreign Model + public $relatedPk; + + // a field or an array of fields that determine which field values + // should be rendered in the selection + public $fields; + + // if this is set, the User is able to select no related model + // if this is set to a string, this string will be presented + public $allowEmpty = 0; + + // Preselect which items? + public $preselect = array(); + + // disable this to hide the Add Button + // set this to a string to set the String to be displayed + public $showAddButton = true; + public $addButtonLink = ''; + // Set this to false to generate a Link rather than a LinkButton + // This is useful when Javascript is not available + public $useLinkButton = true; + + // use this to set the link where the user should return to after + // clicking the add Button + public $returnLink; + + // How should a data row be rendered. {id} will be replaced by the id of + // the model. You can also insert every field that is available in the + // parent object. + // Use {fields} to display all fields delimited by $this->delimiter + // Use {func0} to {funcX} to evaluate user-contributed functions with the + // $functions array. Example: + // + // 'functions' => array( "CHtml::checkBoxList('parent{id}', '', + // CHtml::listData(Othermodel::model()->findAll(), 'id', 'title'));",), + // 'template' => '#{id} : {fields} ({title}) Allowed other Models: {func0}', + public $template = '{fields}'; + + // User-Contributed functions to be evaluated in template + public $functions = array(); + + // how should multiple fields be delimited + public $delimiter = " | "; + + // style of the selection Widget + public $style = "dropDownList"; + public $htmlOptions = array(); + public $parentObjects = 0; + public $orderParentsBy = 0; + public $groupParentsBy = 0; + + // override this for complicated MANY_MANY relations: + public $manyManyTable = ''; + public $manyManyTableLeft = ''; + public $manyManyTableRight = ''; + + public function init() + { + if(!is_object($this->model)) + { + if(!$this->_model = new $this->model) + throw new CException( + Yii::t('yii','Relation widget is not able to instantiate the given Model')); + } + else + { + $this->_model = $this->model; + } + + // Instantiate Model and related Model + foreach($this->_model->relations() as $key => $value) + { + if(strcmp($this->relation,$key) == 0) + { + // $key = Name of the Relation + // $value[0] = Type of the Relation + // $value[1] = Related Model + // $value[2] = Related Field or Many_Many Table + switch($value[0]) + { + case 'CBelongsToRelation': + case 'CHasOneRelation': + $this->_relatedModel = new $value[1]; + if(!isset($this->field)) + { + $this->field = $value[2]; + } + break; + case 'CManyManyRelation': + preg_match_all('/^.*\(/', $value[2], $matches); + $this->manyManyTable = substr($matches[0][0], 0, strlen($matches[0][0]) -1); + preg_match_all('/\(.*,/', $value[2], $matches); + $this->manyManyTableLeft = substr($matches[0][0], 1, strlen($matches[0][0]) - 2); + preg_match_all('/,.*\)/', $value[2], $matches); + $this->manyManyTableRight = substr($matches[0][0], 2, strlen($matches[0][0]) - 3); + + $this->_relatedModel = new $value[1]; + break; + } + } + } + + if(!is_object($this->_relatedModel)) + throw new CException( + Yii::t('yii','Relation widget cannot find the given Relation('.$this->relation.')')); + + if(!isset($this->relatedPk) || $this->relatedPk == "") + { + $this->relatedPk = $this->_relatedModel->tableSchema->primaryKey; + } + + if(!isset($this->fields) || $this->fields == "" || $this->fields == array()) + throw new CException(Yii::t('yii','Widget "Relation" has been run without fields Option(string or array)')); + } + + // Check if model-value contains '.' and generate -> directives: + public function getModelData($model, $field) + { + if(strstr($field, '.')) + { + $data = explode('.', $field); + $value = $model->getRelated($data[0])->$data[1]; + } else + $value = $model->$field; + + return $value; + } + + /** + * This function fetches all needed data of the related Object and returns them + * in an array that is prepared for use in ListData. + */ + public function getRelatedData() + { + /* At first we determine, if we want to display all parent Objects, or + * if the User supplied an list of Objects */ + if(is_object($this->parentObjects)) // a single Element + { + $parentobjects = array($this->parentObjects); + } + else if(is_array($this->parentObjects)) // Only show this elements + { + $parentobjects = $this->parentObjects; + } + else // Show all Parent elements + { + $parentobjects = CActiveRecord::model(get_class($this->_relatedModel))->findAll(); + } + + if($this->allowEmpty) + if(is_string($this->allowEmpty)) + $dataArray[0] = $this->allowEmpty; + else + $dataArray[0] = Yii::t('app', 'None'); + + foreach($parentobjects as $obj) + { + if(!is_array($this->fields)) + $this->fields = array($this->fields); + + $fields = ''; + foreach($this->fields as $field) + { + $rule = sprintf('{%s}',$field); + $rules[$rule] = $obj->$field; + $fields .= $this->getModelData($obj, $field); + if(count($this->fields) >1) + $fields .= $this->delimiter; + } + + $defaultrules = array( + '{fields}' => $fields, + '{id}' => $obj->{$obj->tableSchema->primaryKey}); + + // Look for user-contributed functions and evaluate them + if($this->functions != array()) + { + foreach($this->functions as $key => $function) + { + $funcrules[sprintf('{func%d}', $key)] = CComponent::evaluateExpression( + strtr($function, $defaultrules)); + } + } + + // Merge the evaluated rules, if exist + if(isset($funcrules)) + $rules = array_merge($rules, $funcrules); + + // Merge the default rules into our ruleset + $rules = array_merge($rules, $defaultrules); + + // Apply the rules to the template + $value = strtr($this->template, $rules); + + if($this->groupParentsBy != '') + { + $dataArray[$obj->{$this->groupParentsBy}][$obj->{$this->relatedPk}] = $value; + } + else + { + $dataArray[$obj->{$this->relatedPk}] = $value; + } + } + + if(!isset($dataArray) || !is_array($dataArray)) + $dataArray = array(); + + return $dataArray; + } + + + /** + * Retrieves the Assigned Objects of the MANY_MANY related Table + */ + public function getAssignedObjects() + { + if(!$this->_model->{$this->_model->tableSchema->primaryKey}) + return array(); + + $sql = sprintf("select * from %s where %s = %s", + $this->manyManyTable, + $this->manyManyTableLeft, + $this->_model->{$this->_model->tableSchema->primaryKey}); + + $result = Yii::app()->db->createCommand($sql)->queryAll(); + + foreach($result as $foreignObject) { + $id = $foreignObject[$this->manyManyTableRight]; + $objects[$id] = $this->_relatedModel->findByPk($id); + } + + return isset($objects) ? $objects : array(); + } + + /** + * Retrieves the not Assigned Objects of the MANY_MANY related Table + * This is used in the two-pane style view. + */ + public function getNotAssignedObjects() + { + foreach($this->getRelatedData() as $key => $value) + { + if(!array_key_exists($key, $this->getAssignedObjects())) + { + $objects[$key] = $this->_relatedModel->findByPk($key); + } + } + + return $objects ? $objects : array(); + } + + /** + * Gets the Values of the given Object or Objects depending on the + * $this->fields the widget requests + */ + public function getObjectValues($objects) + { + if(is_array($objects)) { + foreach($objects as $object) + { + $attributeValues[$object->primaryKey] = $object->{$this->fields}; + } + } + else if(is_object($objects)) + { + $attributeValues[$object->primaryKey] = $objects->{$this->fields}; + } + + return isset($attributeValues) ? $attributeValues : array(); + } + + /* + * How will the Listbox of the MANY_MANY Assignment be called? + */ + public function getListBoxName($ajax = false) + { + if($ajax) + { + return sprintf('%s_%s', + get_class($this->_model), + get_class($this->_relatedModel) + ); + } + else + { + return sprintf('%s[%s]', + get_class($this->_model), + get_class($this->_relatedModel) + ); + } + } + + public function renderBelongsToSelection() { + if(strcasecmp($this->style, "dropDownList") == 0) + echo CHtml::ActiveDropDownList( + $this->_model, + $this->field, + $this->getRelatedData(), + $this->htmlOptions); + else if(strcasecmp($this->style, "listbox") == 0) + echo CHtml::ActiveListBox( + $this->_model, + $this->field, + $this->getRelatedData(), + $this->htmlOptions); + else if(strcasecmp($this->style, "checkbox") == 0) + echo CHtml::ActiveCheckBoxList( + $this->_model, + $this->field, + $this->getRelatedData(), + $this->htmlOptions); + + } + + public function renderManyManySelection() { + if(strcasecmp($this->style, 'twopane') == 0) + $this->renderTwoPaneSelection(); + else if(strcasecmp($this->style, 'checkbox') == 0) + $this->renderCheckBoxListSelection(); + else if(strcasecmp($this->style, 'dropDownList') == 0) + $this->renderManyManyDropDownListSelection(); + else + $this->renderOnePaneSelection(); + } + + + /* + * Renders one dropDownList per selectable related Element. + * The users can add additional entries with the + and remove entries + * with the - Button . + */ + public function renderManyManyDropDownListSelection() + { + $i = 0; + foreach($this->_relatedModel->findAll() as $obj) + { + $i++; + $isAssigned = $this->isAssigned($obj->id); + + echo CHtml::openTag('div', array( + 'id' => 'div' . $i, + 'style' => $isAssigned ? '' : 'display:none;', + )); + echo CHtml::dropDownList('rel-' . $obj->id . "-" . $this->getListBoxName(), + $isAssigned ? $obj->id : 0, + CHtml::listData( + array_merge( + array('0' => $this->allowEmpty), + $this->_relatedModel->findAll()), + $this->relatedPk, + $this->fields + ) + ); + echo CHtml::closeTag('div'); + } + + $jsadd = ' + i = 1; + maxi = '.$i.'; + $(\'#add\').click(function() { +$(\'#div\' + i).show(); +if(i <= maxi) ++i; +}); +'; + + $jssub = ' +$(\'#sub\').click(function() { +if(i > 2) --i; +$(\'#div\' + i).hide(); +}); +'; + + Yii::app()->clientScript->registerScript('addbutton', $jsadd); + Yii::app()->clientScript->registerScript('subbutton', $jssub); + + echo CHtml::button('+', array('id' => 'add')); + echo ' '; + echo CHtml::button('-', array('id' => 'sub')); + echo ' '; + } + + public function isAssigned($id) + { + return in_array($id, array_keys($this->getAssignedObjects())); + } + + public static function retrieveValues($data, $field) + { + $returnArray = array(); + + foreach($data as $key => $value) + { + if(strpos($key, 'rel') !== false) + { + if(isset($value[$field])) + $returnArray[] = $value[$field]; + } + } + + return $returnArray; + } + + +public function renderCheckBoxListSelection() +{ + $keys = array_keys($this->getAssignedObjects()); + + if(isset($this->preselect)) + $keys = $this->preselect; + + echo CHtml::CheckBoxList($this->getListBoxName(), + $keys, + $this->getRelatedData(), + $this->htmlOptions); + } + + + public function renderOnePaneSelection() + { + $keys = array_keys($this->getAssignedObjects()); + + echo CHtml::ListBox($this->getListBoxName(), + $keys, + $this->getRelatedData(), + array('multiple' => 'multiple')); + } + + public function handleAjaxRequest($_POST) { + print_r($_POST); + } + + public function renderTwoPaneSelection() + { + echo CHtml::ListBox($this->getListBoxName(), + array(), + $this->getObjectValues($this->getAssignedObjects()), + array('multiple' => 'multiple')); + + $ajax = + array( + 'type'=>'POST', + 'data'=>array('yeah'), + 'update'=>'#' . $this->getListBoxName(true), + ); + + echo CHtml::ajaxSubmitButton('<<', + array('assign'), + $ajax + ); + + $ajax = + array( + 'type'=>'POST', + 'update'=>'#not_'.$this->getListBoxName(true) + ); + + echo CHtml::ajaxSubmitButton('>>', + array('assign','revoke'=>1), + $ajax);//, + //$data['revoke']); + + + echo CHtml::ListBox('not_' . $this->getListBoxName(), + array(), + $this->getObjectValues($this->getNotAssignedObjects()), + array('multiple' => 'multiple')); + } + +public function run() +{ + if($this->manyManyTable != '') + $this->renderManyManySelection(); + else + $this->renderBelongsToSelection(); + + if($this->showAddButton !== false) + { + $this->renderAddButton(); + } +} +protected function renderAddButton() +{ + if(!isset($this->returnLink) or $this->returnLink == "") + $this->returnLink = $this->model->tableSchema->name . "/create"; + + if($this->addButtonLink != '') + $link = $this->addButtonLink; + else + $link = array( + $this->_relatedModel->tableSchema->name . "/create", + 'returnTo' => $this->returnLink); + + + if(!$this->useLinkButton) + { + echo CHtml::Link( + is_string($this->showAddButton) ? $this->showAddButton : 'New', $link + ); + } + else + { + echo CHtml::LinkButton( + is_string($this->showAddButton) ? $this->showAddButton : 'New', + array('submit' => $link)); + } + +} +} + |
