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
| Item1 | Item2 | ...
+ *
+ * @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