diff --git a/www/protected/extensions/fixtureHelper/FixtureHelperCommand.php b/www/protected/extensions/fixtureHelper/FixtureHelperCommand.php new file mode 100644 index 0000000..eb1f4ab --- /dev/null +++ b/www/protected/extensions/fixtureHelper/FixtureHelperCommand.php @@ -0,0 +1,60 @@ +fixture = Yii::app()->getComponent('fixture'); + $this->fixture->basePath = Yii::getPathOfAlias($alias.'.tests.fixtures'); + $this->fixture->init(); + + $tables = explode(',', $tables); + foreach ($tables as $table) { + try { + $this->fixture->resetTable($table); + $this->fixture->loadFixture($table); + } catch (Exception $e) { + echo "ERROR: There is a problem working with the table $table. ". + "Is it spelled correctly or exist?\n\n"; + } + } + echo "Done.\n\n"; + } + +} \ No newline at end of file diff --git a/www/protected/extensions/fixtureHelper/README.markdown b/www/protected/extensions/fixtureHelper/README.markdown new file mode 100644 index 0000000..5327a81 --- /dev/null +++ b/www/protected/extensions/fixtureHelper/README.markdown @@ -0,0 +1,45 @@ +FixtureHelper for Yii Framework +=============================== + +FixtureHelper is a command application lets you work with your fixtures outside +testing. Currently what it does is just helping you to load you fixtures from your +fixture files to your database, without the need to invoke PHPUnit. + +INSTALL +------- +Copy FixtureHelperCommand.php and place it under `protected/extensions/fixtureHelper/` + +Edit `protected/config/console.php`, add the following to the config array under +first dimension: + + 'commandMap' => array( + 'fixture' => array( + 'class'=>'application.extensions.fixtureHelper.FixtureHelperCommand', + ), + ), + +Configure your database by setting up your db under `components`. + +Add the following inside `components`. + + 'fixture'=>array( + 'class'=>'system.test.CDbFixtureManager', + ), + +USAGE +------ +fixture load [--alias=folderalias] --table=tablename1[,tablename2[,...]] + +PARAMETERS +----------- +* load: Load fixtures into the database +* --alias: The alias to the directory that contains "models" and "tests" + folders. Please note that folder "models" should contain the Model class of + the fixtures to be loaded. Defaults to "application". Optional for "load". +* --tables: Name of the tables to be loaded with your defined fixtures. Name + values are comma separated. Required for "load". + +EXAMPLES +-------- + + yiic fixture load --alias=application.modules.mymodule --tables=fruit,transport,country \ No newline at end of file diff --git a/www/protected/extensions/multimodelform/MultiModelForm.php b/www/protected/extensions/multimodelform/MultiModelForm.php new file mode 100644 index 0000000..01298d3 --- /dev/null +++ b/www/protected/extensions/multimodelform/MultiModelForm.php @@ -0,0 +1,1161 @@ + + * @copyright 2011 myticket it-solutions gmbh + * @license New BSD License + * @category User Interface + * @version 3.0 + */ +class MultiModelForm extends CWidget +{ + const CLASSPREFIX = 'mmf_'; //prefix for tag classes + + /** + * The model to handle + * + * @var CModel $model + */ + public $model; + + /** + * Configuration of the form provided by the models method getMultiModelForm() + * + * This configuration array defines generation CForm + * Can be a config array or a config file that returns the configuration + * + * @link http://www.yiiframework.com/doc/guide/1.1/en/form.builder + * @var mixed $elements + */ + public $formConfig = array(); + + /** + * Array of models loaded from db. + * Created for example by $model->findAll(); + * + * @var CModel $data + */ + public $data; + + /** + * The controller returns all validated items (array of model) + * if a validation error occurs. + * The form will then be rendered with error output. + * $data will be ignored in this case. + * @see method run() + * + * @var array $validatedItems + */ + public $validatedItems; + + /** + * Set to true if the error summary should be rendered for the model of this form + * + * @var boolean $showErrorSummary + */ + public $showErrorSummary = false; + + + /** + * The text of the copy/clone link + * + * @var string $addItemText + */ + public $addItemText = 'Add item'; + + /** + * Show 'Add item' link and empty item in errormode + * + * @var boolean $allowAddOnError + */ + public $showAddItemOnError = true; + + /** + * The text for the remove link + * Can be an image tag too. + * Leave empty to disable removing. + * + * @var string $removeText + */ + public $removeText = 'Remove'; + + /** + * The confirmation text before remove an item + * Set to null/empty to disable confirmation + * + * @var string $removeText + */ + public $removeConfirm = 'Delete this item?'; + + /** + * The htmlOptions for the remove link + * + * @var array $removeHtmlOptions + */ + public $removeHtmlOptions = array(); + + /** + * Show elements as table + * If set to true, $fieldsetWrapper, $rowWrapper and $removeLinkWrapper will be ignored + * + * @var boolean + */ + public $tableView = false; + + /** + * The htmlOptions for the table tag + * + * @var array $tableHtmlOptions + */ + public $tableHtmlOptions = array(); + + /** + * Items are rendered as Item1Item2 ... + * + * @var string $tableFootCells + */ + public $tableFootCells = array(); + + + /** + * Set this attribute to enable manual sorting by drag/drop of the multiple items + * Uses the CJuiSortable widget + * + * @var string the name of the attribute + */ + public $sortAttribute; + + + /** + * The options property of the zii.widgets.jui.CJuiSortable + * + * @link http://www.yiiframework.com/doc/api/1.1/CJuiWidget#options-detail + * @link http://jqueryui.com/demos/sortable/ + * + * @var array + */ + public $sortOptions = array( + 'placeholder' => 'ui-state-highlight', + 'opacity' => 0.8, + 'cursor' => 'move' + ); + + /** + * The wrapper for each fieldset + * + * @var array $fieldsetWrapper + */ + public $fieldsetWrapper = array( + 'tag' => 'div', + 'htmlOptions' => array('class' => 'view'), //'fieldset' is unknown in the default css context of form.css + ); + + /** + * The wrapper for a row + * + * @var array $rowWrapper + */ + public $rowWrapper = array( + 'tag' => 'div', + 'htmlOptions' => array('class' => 'row'), + ); + + /** + * The wrapper for the removeLink + * + * @var array $fieldsetWrapper + */ + public $removeLinkWrapper = array( + 'tag' => 'span', + 'htmlOptions' => array(), + ); + + /** + * The javascript code jsBeforeClone,jsAfterClone ... + * This allows to handle widgets on cloning. + * Important: 'this' is the current handled jQuery object + * For CJuiDatePicker and extension 'datetimepicker' see prepared php-code below: afterNewIdDatePicker,afterNewIdDateTimePicker + * + * Usage if you have CJuiDatePicker to clone (assume your form elements are defined in the array $formConfig): + * 'jsAfterNewId' => MultiModelForm::afterNewIdDateTimePicker($formConfig['elements']['mydatefield']), + * + */ + public $jsBeforeClone; // 'jsBeforeClone' => "alert(this.attr('class'));"; + public $jsAfterClone; // 'jsAfterClone' => "alert(this.attr('class'));"; + public $jsBeforeNewId; // 'jsBeforeNewId' => "alert(this.attr('id'));"; + public $jsAfterNewId; // 'jsAfterNewId' => "alert(this.attr('id'));"; + + /** + * Available options for the jQuery plugin RelCopy + * + * string excludeSelector - A jQuery selector used to exclude an element and its children + * integer limit - The number of allowed copies. Default: 0 is unlimited + * string append - Additional HTML to attach at the end of each copy. + * string copyClass - A class to attach to each copy + * boolean clearInputs - Option to clear each copies text input fields or textarea + * + * @link http://www.andresvidal.com/labs/relcopy.html + * + * @var array $options + */ + public $options = array(); + + /** + * The assets url + * + * @var string $_assets + */ + private $_assets; + + /** + * Support for CJuiDatePicker + * Set 'jsAfterNewId'=MultiModelForm::afterNewIdDateTimePicker($myFormConfig['elements']['mydate']) + * if you use at least one datepicker. + * + * The options will be assigned from the config array of the element + * + * @param array $element + * @return string + */ + public static function afterNewIdDatePicker($element) + { + $options = isset($element['options']) ? $element['options'] : array(); + $jsOptions = CJavaScript::encode($options); + + $language = isset($element['language']) ? $element['language'] : ''; + if (!empty($language)) + $language = "jQuery.datepicker.regional['$language'],"; + + return "if(this.hasClass('hasDatepicker')) {this.removeClass('hasDatepicker'); this.datepicker(jQuery.extend({showMonthAfterYear:false}, $language {$jsOptions}));};"; + } + + /** + * Support for extension datetimepicker + * @link http://www.yiiframework.com/extension/datetimepicker/ + * + * @param array $element + * @return string + */ + public static function afterNewIdDateTimePicker($element) + { + $options = isset($element['options']) ? $element['options'] : array(); + $jsOptions = CJavaScript::encode($options); + + $language = isset($element['language']) ? $element['language'] : ''; + if (!empty($language)) + $language = "jQuery.datepicker.regional['$language'],"; + + return "if(this.hasClass('hasDatepicker')) {this.removeClass('hasDatepicker').datetimepicker(jQuery.extend($language {$jsOptions}));};"; + } + + /** + * Support for CJuiAutoComplete: not working - needs review + * + * @param array $element + * @return string + */ + + public static function afterNewIdAutoComplete($element) + { + $options = isset($element['options']) ? $element['options'] : array(); + if (isset($element['sourceUrl'])) + $options['source'] = CHtml::normalizeUrl($element['sourceUrl']); + else + $options['source'] = $element['source']; + + $jsOptions = CJavaScript::encode($options); + + //return "this.autocomplete($jsOptions);"; //works for non-autocomplete elements + //return "if(this.hasClass('ui-autocomplete-input')) this.autocomplete($jsOptions);"; + //return "if(this.hasClass('ui-autocomplete-input')) $('#'+this.attr('id')).autocomplete($jsOptions);"; + //return "if(this.hasClass('ui-autocomplete-input')) $('#'+this.attr('id')).autocomplete('destroy').autocomplete($jsOptions);"; + //return "if(this.hasClass('ui-autocomplete-input')) $('#'+this.attr('id')).unbind().removeClass('ui-autocomplete-input').removeAttr('autocomplete').removeAttr('role').removeAttr('aria-autocomplete').removeAttr('aria-haspopup').autocomplete($jsOptions);"; + //return "if(this.hasClass('ui-autocomplete-input')) this.unbind().removeClass('ui-autocomplete-input').removeAttr('autocomplete').removeAttr('role').removeAttr('aria-autocomplete').removeAttr('aria-haspopup').autocomplete($jsOptions);"; + } + + /** + * This static function should be used in the controllers update action + * The models will be validated before saving + * + * If a record is not valid, the invalid model will be set to $model + * to display error summary + * + * @param mixed $model CActiveRecord or other CModel + * @param array $validatedItems returns the array of validated records + * @param array $deleteItems + * @param array $masterValues attributes to assign before saving + * @param array $formData (default = $_POST) + * @return boolean + */ + public static function save($model, &$validatedItems, &$deleteItems = array(), $masterValues = array(), $formData = null) + { + //validate if empty: means no validation has been done + $doValidate = empty($validatedItems) && empty($deleteItems); + + if (!isset($formData)) + $formData = $_POST; + + $sortAttribute = !empty($formData[self::CLASSPREFIX . 'sortAttribute']) ? $formData[self::CLASSPREFIX . 'sortAttribute'] : null; + $sortIndex = 0; + + if ($doValidate) + { + //validate and assign $masterValues + if (!self::validate($model, $validatedItems, $deleteItems, $masterValues, $formData)) + return false; + } + + if (!empty($validatedItems)) + foreach ($validatedItems as $item) + { + if (!$doValidate) //assign $masterValues + { + if (!empty($masterValues)) + $item->setAttributes($masterValues, false); + } + + //if sortable, assign the sortAttribute + if (!empty($sortAttribute)) + { + $sortIndex++; + $item->$sortAttribute = $sortIndex; + } + + if (!$item->save()) + return false; + } + + //$deleteItems = array of primary keys to delete + if (!empty($deleteItems)) + foreach ($deleteItems as $pk) + if (!empty($pk)) + { + //array doesn't work with activerecord? + if (count($pk == 1)) + { + $vals = array_values($pk); + $pk = $vals[0]; + } + + $model->deleteByPk($pk); + } + + return true; + } + + /** + * Validates submitted formdata + * If a record is not valid, the invalid model will be set to $model + * to display error summary + * + * @param mixed $model + * @param array $validatedItems returns the array of validated records + * @param array $deleteItems returns the array of model for deleting + * @param array $masterValues attributes to assign before saving + * @param array $formData (default = $_POST) + * @return boolean + */ + public static function validate($model, &$validatedItems, &$deleteItems = array(), $masterValues = array(), $formData = null) + { + $widget = new MultiModelForm; + $widget->model = $model; + + $widget->checkModel(); + + if (!$widget->initItems($validatedItems, $deleteItems, $masterValues, $formData)) + return false; //at least one item is not valid + else + return true; + } + + /** + * Converts the submitted formdata into an array of model + * + * @param array $formData the postdata $_POST submitted by the form + * @param array $validatedItems the items which were validated + * @param array $deleteItems the items to delete + * @param array $masterValues assign additional masterdata before save + * @return array array of model + */ + public function initItems(&$validatedItems, &$deleteItems, $masterValues = array(), $formData = null) + { + if (!isset($formData)) + $formData = $_POST; + + $result = true; + $newItems = array(); + + $validatedItems = array(); //bugfix: 1.0.2 + $deleteItems = array(); + + $modelClass = get_class($this->model); + + if (!isset($formData) || empty($formData[$modelClass])) + return true; + + //----------- NEW (on validation error) ----------- + + if (isset($formData[$modelClass]['n__'])) + { + foreach ($formData[$modelClass]['n__'] as $idx => $attributes) + { + $model = new $modelClass; + $model->attributes = $attributes; + + if (!empty($masterValues)) + $model->setAttributes($masterValues, false); //assign mastervalues + + // validate + if (!$model->validate()) + $result = false; + + $validatedItems[] = $model; + } + + unset($formData[$modelClass]['n__']); + } + + //----------- UPDATE ----------- + + $allExistingPk = isset($formData[$modelClass]['pk__']) ? $formData[$modelClass]['pk__'] : null; //bugfix: 1.0.1 + + if (isset($formData[$modelClass]['u__'])) + { + foreach ($formData[$modelClass]['u__'] as $idx => $attributes) + { + $model = new $modelClass('update'); + + //should work for CModel, mongodb models... too + if (method_exists($model, 'setIsNewRecord')) + $model->setIsNewRecord(false); + + $model->attributes = $attributes; + + if (!empty($masterValues)) + $model->setAttributes($masterValues, false); //assign mastervalues + + //ensure to assign primary keys (when pk is unsafe or not defined in rules) + if (is_array($allExistingPk)) + { + $primaryKeys = $allExistingPk[$idx]; + $model->setAttributes($primaryKeys, false); + } + + // validate + if (!$model->validate()) + $result = false; + + $validatedItems[] = $model; + + // remove from $allExistingPk + if (is_array($allExistingPk)) + unset($allExistingPk[$idx]); + } + + unset($formData[$modelClass]['u__']); + } + + //----------- DELETE ----------- + + // add remaining primarykeys to $deleteItems (reindex) + if (is_array($allExistingPk)) + foreach ($allExistingPk as $idx => $delPks) + $deleteItems[] = $delPks; + + // remove handled formdata pk__ + unset($formData[$modelClass]['pk__']); + + //----------- Check for cloned elements by jQuery ----------- + if(!empty($formData[$modelClass])) //has cloned elements + { + // use the first item as reference + $refAttribute = key($formData[$modelClass]); + $refArray = array_shift($formData[$modelClass]); + + if (!empty($refArray)) + foreach ($refArray as $idx => $value) + { + // check continue if all values are empty + if (empty($value)) + { + $allEmpty = true; + foreach ($formData[$modelClass] as $attrKey => $values) + { + if (is_array($values[$idx])) //bugfix v2.1.1 have to check empty array items too + { + $isEmpty = true; + foreach ($formData[$modelClass][$attrKey] as $item) + { + if (!empty($item[$idx])) + { + $isEmpty = false; + break; + } + } + } + else + $isEmpty = empty($values[$idx]); + + $allEmpty = $isEmpty && $allEmpty; + if (!$allEmpty) + break; + } + + if ($allEmpty) + continue; + } + + $model = new $modelClass; + $model->$refAttribute = $value; + + foreach ($formData[$modelClass] as $attrKey => $values) + { + //v2.2 support for checkboxlist / radiolist + if (is_array($values[$idx])) + { + $arrayAttribute = array(); + foreach ($formData[$modelClass][$attrKey] as $item) + { + if (!empty($item[$idx])) + $arrayAttribute[] = $item[$idx]; + } + + $model->$attrKey = $arrayAttribute; + } + else + $model->$attrKey = $values[$idx]; + } + + //assign mastervalues without checking rules for new records + $model->setAttributes($masterValues, false); + + // validate + if (!$model->validate()) + $result = false; + + $validatedItems[] = $model; + } + } + + return $result; + } + + /** + * Get the primary key as array (key => value) + * + * @param CModel $model + * @return array + */ + public function getPrimaryKey($model) + { + $result = array(); + + if ($model instanceof CActiveRecord) + { + $pkValue = $model->primaryKey; + if (!empty($pkValue)) + { + $pkName = $model->primaryKey(); + if (empty($pkName)) + $pkName = $model->tableSchema->primaryKey; + + $result = array($pkName => $pkValue); + } + } + else // when working with EMongoDocument + if (method_exists($model, 'primaryKey')) + { + $pkName = $model->primaryKey(); + $pkValue = $model->$pkName; + if (empty($pkValue)) + $result = array($pkName => $pkValue); + } + + return $result; + } + + + /** + * Get the copyClass + * + * @return string + */ + public function getCopyClass() + { + if (isset($this->options['copyClass'])) + return $this->options['copyClass']; + else + { + $selector = $this->id . '_copy'; + $this->options['copyClass'] = $selector; + return $selector; + } + } + + /** + * The link for removing a fieldset + * + * @return string + */ + public function getRemoveLink() + { + if (empty($this->removeText)) + return ''; + + $onClick = 'jQuery(this).parent().parent().remove(); return false;'; + + if (!empty($this->removeConfirm)) + $onClick = "if(confirm('{$this->removeConfirm}')) " . $onClick; + + $htmlOptions = array_merge($this->removeHtmlOptions, array('onclick' => $onClick)); + $link = CHtml::link($this->removeText, '#', $htmlOptions); + + return CHtml::tag($this->removeLinkWrapper['tag'], + $this->removeLinkWrapper['htmlOptions'], $link); + } + + /** + * Check if rows has to be sortable + * Works only if not is as tableView because the submitted $_POST data are not in the correct sorted order + * Sorting in tableView needs more investigation/workaround ... + * + * @return bool + */ + public function isSortable() + { + return !empty($this->sortAttribute) && !$this->tableView; + } + + /** + * Initialize the widget: register scripts + */ + public function init() + { + if ($this->tableView) + { + $this->fieldsetWrapper = array('tag' => 'tr', 'htmlOptions' => array('class' => self::CLASSPREFIX . 'row')); + $this->rowWrapper = array('tag' => 'td', 'htmlOptions' => array('class' => self::CLASSPREFIX . 'cell')); + $this->removeLinkWrapper = $this->rowWrapper; + } + + $this->checkModel(); + $this->registerClientScript(); + parent::init(); + } + + + /** + * Check the model instance on init / after create + * Add all model attributes as hidden and visible=false if they are not part of the formConfig + * Need this because on update all attributes have to be submitted, no 'loadModel' is called + */ + protected function checkModel() + { + if (is_string($this->model)) + $this->model = new $this->model; + + if (isset($this->model) && isset($this->formConfig)) + { + // add undefined attributes in the form config as hidden fields and attribute visible = false + if (isset($this->formConfig) && !empty($this->formConfig['elements'])) + foreach ($this->model->attributes as $attribute => $value) + { + if (!array_key_exists($attribute, $this->formConfig['elements'])) + $this->formConfig['elements'][$attribute] = array('type' => 'hidden', 'visible' => false); + } + } + } + + + /** + * @return array the javascript options + */ + protected function getClientOptions() + { + if (empty($this->options)) + $this->options = array(); + + if (!empty($this->removeText)) + { + $append = $this->getRemoveLink(); + $this->options['append'] = empty($this->options['append']) ? $append : $append . ' ' . $this->options['append']; + } + + if (!empty($this->jsBeforeClone)) + $this->options['beforeClone'] = $this->jsBeforeClone; + + if (!empty($this->jsAfterClone)) + $this->options['afterClone'] = $this->jsAfterClone; + + if (!empty($this->jsBeforeNewId)) + $this->options['beforeNewId'] = $this->jsBeforeNewId; + + if (!empty($this->jsAfterNewId)) + $this->options['afterNewId'] = $this->jsAfterNewId; + + return CJavaScript::encode($this->options); + + } + + /** + * The id selector for jQuery.sortable + * + * @return string + */ + protected function getSortSelectorId() + { + return self::CLASSPREFIX . 'sortable'; + } + + /** + * Registers the relcopy javascript file. + */ + public function registerClientScript() + { + $cs = Yii::app()->getClientScript(); + + $this->_assets = Yii::app()->assetManager->publish(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'assets'); + + $cs->registerCoreScript('jquery'); + $cs->registerScriptFile($this->_assets . '/js/jquery.relcopy.yii.1.0.js'); + + $options = $this->getClientOptions(); + $cs->registerScript(__CLASS__ . '#' . $this->id, "jQuery('#{$this->id}').relCopy($options);"); + + //add the script for jQuery.sortable + if ($this->isSortable()) + { + $cs->registerCoreScript('jquery.ui'); + $cssFile = $cs->getCoreScriptUrl() . '/jui/css/base/jquery-ui.css'; + $cs->registerCssFile($cssFile); + + $options = CJavaScript::encode($this->sortOptions); + Yii::app()->getClientScript()->registerScript(__CLASS__ . '#' . $this->id . 'Sortable', "jQuery('#{$this->getSortSelectorId()}').sortable({$options});"); + } + } + + /** + * Render the top of the table: AddLink, Table header + */ + public function renderTableBegin($renderAddLink) + { + $form = new MultiModelRenderForm($this->formConfig, $this->model); + $form->parentWidget = $this; + + //add link as div + if ($renderAddLink) + { + $addLink = $form->getAddLink(); + echo CHtml::tag('div', array('class' => self::CLASSPREFIX . 'addlink'), $addLink); + } + + $tableHtmlOptions = array_merge(array('class' => self::CLASSPREFIX . 'table'), $this->tableHtmlOptions); + + //table + echo CHtml::tag('table', $tableHtmlOptions, false, false); + + //thead + $form->renderTableHeader(); + + //tfoot + if (!empty($this->tableFootCells)) + { + $cells = ''; + foreach ($this->tableFootCells as $cell) + { + $cells .= CHtml::tag('td', array('class' => self::CLASSPREFIX . 'cell'), $cell); + } + + $cells = CHtml::tag('tr', array('class' => self::CLASSPREFIX . 'row'), $cells); + echo CHtml::tag('tfoot', array(), $cells); + } + + //tbody + $tbodyOptions = $this->isSortable() ? array('id' => $this->getSortSelectorId()) : array(); + echo CHtml::tag('tbody', $tbodyOptions, false, false); + } + + + /** + * Renders the active form if a model and formConfig is set + * $this->data is array of model + */ + public function run() + { + if (empty($this->model) || empty($this->formConfig)) + return; + + //form is displayed again with some invalid models + $isErrorMode = !empty($this->validatedItems); + $showAddLink = !$isErrorMode || ($isErrorMode && $this->showAddItemOnError); + + $this->formConfig['activeForm'] = array('class' => 'MultiModelEmbeddedForm'); + + $idx = 0; + $errorPk = null; + + + if ($isErrorMode) + { + if ($this->showErrorSummary) + echo CHtml::errorSummary($this->validatedItems); + + $data = $this->validatedItems; + } + else + $data = $this->data; //from the db + + + if ($this->tableView) + $this->renderTableBegin(false); //RODAX $showAddLink); + + if ($this->isSortable()) + { + //render the name of the sortAttribute as hidden input + //used in MultiModelForm::save + echo CHtml::hiddenField(self::CLASSPREFIX . 'sortAttribute', $this->sortAttribute); + + if (!$this->tableView) + echo CHtml::openTag('div', array('id' => $this->getSortSelectorId())); + } + + // existing records + if (is_array($data) && !empty($data)) + { + foreach ($data as $model) + { + $form = new MultiModelRenderForm($this->formConfig, $model); + $form->index = $idx; + $form->parentWidget = $this; + + $form->primaryKey = $this->getPrimaryKey($model); + + if (!$this->tableView) + { + if ($showAddLink && $idx == 0) // no existing data rendered + echo $form->renderAddLink(); + } + + // render pk outside of removeable tag, for checking records to delete + // see method initItems() + echo $form->renderHiddenPk('[pk__]'); + echo $form->render(); + + $idx++; + } + } + + //if form is displayed first time or in errormode and want to show 'Add item' (and a 'CopyTemplate') + if($showAddLink) + { + // add an empty fieldset as CopyTemplate + $form = new MultiModelRenderForm($this->formConfig, $this->model); + $form->index = $idx; + $form->parentWidget = $this; + $form->isCopyTemplate = true; + + if (!$this->tableView) + { + if ($idx == 0) // no existing data rendered + echo $form->renderAddLink(); + } + + echo $form->render(); + } + + if ($this->tableView) + { + echo CHtml::closeTag('tbody'); + echo CHtml::closeTag('table'); + } + elseif ($this->isSortable()) + echo CHtml::closeTag('div'); + } +} + +/** + * The CForm to render the input form + */ +class MultiModelRenderForm extends CForm +{ + public $parentWidget; + public $index; + public $isCopyTemplate; + public $primaryKey; + + + /** + * Wraps a content with row wrapper + * + * @param string $content + * @return string + */ + protected function getWrappedRow($content) + { + return CHtml::tag($this->parentWidget->rowWrapper['tag'], + $this->parentWidget->rowWrapper['htmlOptions'], $content); + } + + /** + * Wraps a content with fieldset wrapper + * + * @param string $content + * @return string + */ + protected function getWrappedFieldset($content) + { + return CHtml::tag($this->parentWidget->fieldsetWrapper['tag'], + $this->parentWidget->fieldsetWrapper['htmlOptions'], $content); + } + + /** + * Returns the generated label from Yii form builder + * Needs to be replaced by the real attributeLabel + * @see method renderFormElements() + * + * @param string $prefix + * @param string $attributeName + * @return string + */ + protected function getAutoCreatedLabel($prefix, $attributeName) + { + return ($this->model->generateAttributeLabel('[' . $prefix . '][' . $this->index . ']' . $attributeName)); + } + + /** + * Renders the table head + * + * @return string + */ + public function renderTableHeader() + { + $cells = ''; + + foreach ($this->getElements() as $element) + if ($element->visible) + { + $text = empty($element->label) ? ' ' : $element->label; + $options = array(); + + if ($element->getRequired()) + { + $options = array('class' => CHtml::$requiredCss); + $text .= CHtml::$afterRequiredLabel; + } + + $cells .= CHtml::tag('th', $options, $text); + } + + //add an empty column instead of remove link + $cells .= CHtml::tag('th', array(), ' '); + + $row = $this->getWrappedFieldset($cells); + echo CHtml::tag('thead', array(), $cells); + } + + + /** + * Check if elem is a array type + * + * @param string $type + * @return boolean + */ + protected function isElementArrayType($type) + { + switch ($type) + { + case 'checkboxlist': + case 'radiolist': + return true; + default: + return false; + } // switch + } + + /** + * Renders a single form element + * Remove the '[]' from the label + * + * @return string + */ + protected function renderFormElements() + { + $output = ''; + + $elements = $this->getElements(); + + foreach ($elements as $element) + { + if (isset($element->name)) + { + $elemName = $element->name; + $elemLabel = $element->renderLabel(); //get the correct/nice label before changing name + $element->label = ''; //no label on $element->render() + + if ($this->isCopyTemplate) // new fieldset + { + if ($element->visible) + { + //v.2.2 support for checkboxlist radiolist + //Array types have to be rendered as array in the CopyTemplate + $element->name = $this->isElementArrayType($element->type) ? $elemName . '[][]' : $elemName . '[]'; + + $elemOutput = ($element->type == 'hidden' || //bugfix: v2.1.1 + $this->parentWidget->tableView) ? '' : $elemLabel; + + $elemOutput .= $element->render(); + //bugfix: v2.1 - don't render hidden inputs in table cell + $output .= $element->type == 'hidden' ? $elemOutput : $this->getWrappedRow($elemOutput); + } + } + elseif (!empty($this->primaryKey)) + { // existing fieldsets update + + $prefix = 'u__'; + $element->name = '[' . $prefix . '][' . $this->index . ']' . $elemName; + + if ($element->type == 'hidden') + $output .= $element->render(); + else + { + $elemOutput = $this->parentWidget->tableView ? '' : $elemLabel; + $elemOutput .= $element->render(); + $output .= $this->getWrappedRow($elemOutput); + } + } + else + { //in validation error mode: the new added items before + if ($element->visible) + { + $prefix = 'n__'; + $element->name = '[' . $prefix . '][' . $this->index . ']' . $elemName; + + if ($element->type == 'hidden') + $output .= $element->render(); + else + { + $elemOutput = $this->parentWidget->tableView ? '' : $elemLabel; + $elemOutput .= $element->render(); + $output .= $this->getWrappedRow($elemOutput); + } + } + } + } + } + + if (!$this->isCopyTemplate) + $output .= $this->parentWidget->getRemoveLink(); + + return $output; + } + + /** + * Renders the primary key value as hidden field + * Need determine which records to delete + * + * @param string $classSuffix + * @return string + */ + public function renderHiddenPk($classSuffix = '[pk__]') + { + foreach ($this->primaryKey as $key => $value) + { + $modelClass = get_class($this->parentWidget->model); + $name = $modelClass . $classSuffix . '[' . $this->index . ']' . '[' . $key . ']'; + return CHtml::hiddenField($name, $value); + } + } + + /** + * Get the add item link + * + * @return string + */ + public function getAddLink() + { + return CHtml::tag('a', + array('id' => $this->parentWidget->id, + 'href' => '#', + 'rel' => '.' . $this->parentWidget->getCopyClass() + ), + $this->parentWidget->addItemText + ); + } + + /** + * Renders the link 'Add' for cloning the DOM element + * + * @return string + */ + public function renderAddLink() + { + return $this->getWrappedRow($this->getAddLink()); + } + + /** + * Renders the CForm + * Each fieldset is wrapped with the fieldsetWrapper + * + * @return string + */ + public function render() + { + $elemOutput = $this->renderBegin(); + $elemOutput .= $this->renderFormElements(); + $elemOutput .= $this->renderEnd(); + // wrap $elemOutput + $wrapperClass = $this->parentWidget->fieldsetWrapper['htmlOptions']['class']; + + if ($this->isCopyTemplate) + { + $class = empty($wrapperClass) + ? $this->parentWidget->getCopyClass() + : $wrapperClass . ' ' . $this->parentWidget->getCopyClass(); + } + else + $class = $wrapperClass; + + $this->parentWidget->fieldsetWrapper['htmlOptions']['class'] = $class; + return $this->getWrappedFieldset($elemOutput); + } +} + +/** + * MultiModelEmbeddedForm + * + * A CActiveForm with no output of the form begin and close tag + * In Yii 1.1.6 the form end/close is the only output of the methods init() and run() + * Needs review in upcoming releases + * + */ +class MultiModelEmbeddedForm extends CActiveForm +{ + /** + * Initializes the widget. + * Don't render the open tag + */ + public function init() + { + ob_start(); + parent::init(); + ob_get_clean(); + } + + /** + * Runs the widget. + * Don't render the close tag + */ + public function run() + { + ob_start(); + parent::run(); + ob_get_clean(); + } +} \ No newline at end of file diff --git a/www/protected/extensions/multimodelform/assets/js/jquery.relcopy.yii.1.0.js b/www/protected/extensions/multimodelform/assets/js/jquery.relcopy.yii.1.0.js new file mode 100644 index 0000000..9bd3c68 --- /dev/null +++ b/www/protected/extensions/multimodelform/assets/js/jquery.relcopy.yii.1.0.js @@ -0,0 +1,129 @@ +/** + * jquery.relcopy.yii.1.0.js + * Version for Yii extension 'jqrelcopy' and 'multimodelform' + * Added: beforeClone,afterClone,beforeNewId,afterNewId + * @link http://www.yiiframework.com/extension/jqrelcopy + * @link http://www.yiiframework.com/extension/multimodelform + * @author: J. Blocher + * ----------------------------------------------------------------- + * jQuery-Plugin "relCopy" + * + * @version: 1.1.0, 25.02.2010 + * + * @author: Andres Vidal + * code@andresvidal.com + * http://www.andresvidal.com + * + * Instructions: Call $(selector).relCopy(options) on an element with a jQuery type selector + * defined in the attribute "rel" tag. This defines the DOM element to copy. + * @example: $('a.copy').relCopy({limit: 5}); // Copy Phone + * + * @param: string excludeSelector - A jQuery selector used to exclude an element and its children + * @param: integer limit - The number of allowed copies. Default: 0 is unlimited + * @param: string append - HTML to attach at the end of each copy. Default: remove link + * @param: string copyClass - A class to attach to each copy + * @param: boolean clearInputs - Option to clear each copies text input fields or textarea + * + */ + +(function($) { + + $.fn.relCopy = function(options) { + var settings = jQuery.extend({ + excludeSelector: ".exclude", + emptySelector: ".empty", + copyClass: "copy", + append: '', + clearInputs: true, + limit: 0, // 0 = unlimited + beforeClone: null, + afterClone: null, + beforeNewId: null, + afterNewId: null + }, options); + + settings.limit = parseInt(settings.limit); + + // loop each element + this.each(function() { + + // set click action + $(this).click(function(){ + var rel = $(this).attr('rel'); // rel in jquery selector format + var counter = $(rel).length; + + // stop limit + if (settings.limit != 0 && counter >= settings.limit){ + return false; + }; + + var funcBeforeClone = function(){eval(settings.beforeClone);}; + var funcAfterClone = function(){eval(settings.afterClone);}; + var funcBeforeNewId = function(){eval(settings.beforeNewId);}; + var funcAfterNewId = function(){eval(settings.afterNewId);}; + + var master = $(rel+":first"); + funcBeforeClone.call(master); + + var parent = $(master).parent(); + var clone = $(master).clone(true).addClass(settings.copyClass+counter).append(settings.append); + funcAfterClone.call(clone); + + //Remove Elements with excludeSelector + if (settings.excludeSelector){ + $(clone).find(settings.excludeSelector).remove(); + }; + + //Empty Elements with emptySelector + if (settings.emptySelector){ + $(clone).find(settings.emptySelector).empty(); + }; + + // Increment Clone IDs + if ( $(clone).attr('id') ){ + var newid = $(clone).attr('id') + (counter +1); + funcBeforeNewId.call(clone); + $(clone).attr('id', newid); + funcAfterNewId.call(clone); + }; + + // Increment Clone Children IDs + $(clone).find('[id]').each(function(){ + var newid = $(this).attr('id') + (counter +1); + funcBeforeNewId.call($(this)); + $(this).attr('id', newid); + funcAfterNewId.call($(this)); + }); + + //Clear Inputs/Textarea + if (settings.clearInputs){ + $(clone).find(':input').each(function(){ + var type = $(this).attr('type'); + switch(type) + { + case "button": + break; + case "reset": + break; + case "submit": + break; + case "checkbox": + $(this).attr('checked', ''); + break; + default: + $(this).val(""); + } + }); + }; + + $(parent).find(rel+':last').after(clone); + return false; + + }); // end click action + + }); //end each loop + + return this; // return to jQuery + }; + +})(jQuery); \ No newline at end of file diff --git a/www/protected/extensions/multimodelform/license.txt b/www/protected/extensions/multimodelform/license.txt new file mode 100644 index 0000000..cd5b293 --- /dev/null +++ b/www/protected/extensions/multimodelform/license.txt @@ -0,0 +1,28 @@ +Copyright © 2011 by myticket it-solutions gmbh (http://www.myticket.at) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of myticket it-solutions gmbh nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file