Multiple Model Save With Gridview

Hi Everybody,

I have 2 tables to collect data from: one table is Order(s), I select 1 position from this table, and I have a Product(s) table and I would like to select several positions from this as a list view or similar (best would be a grid because of filtering ability), and the user provides additional data in textfields (amounts) for every product (all in one form). And I would like to save all into a 3rd table (Orders_Products).

What is the best way to save multiple models at once like this, keeping the ability to show validation errors right there where they are generated (right there on the form)?

I can make it work, using the two models Order and Products, but I have a little problem with validation. This way I can’t make it to show validation errors right there where they are generated. I mean, for example if a user forgets to provide data for a textfield in only one of the rows regarding Products list, I would like to show validation error right there on the form, but for this, I guess I should include the 3rd model (Orders_Products) also, because in fact it’s the validation error for this model, but this seems a little bit too confusing to me (but maybe possible).

Does it make any sense? If yes, what can be a good and most simple solution for this?

Thanks a lot!

BR

c

I just replied here because I have changed the original reply and it didn’t go up on the list.

have you try this ???

http://www.yiiframework.com/doc/guide/1.1/en/form.builder#creating-a-nested-form

yes, I have seen this, but it’s rather different (more simple) because it operates with only 2 models, but I have to save multiple records for a 3rd model.

nobody?

It’s a bit difficult challenge, I think.

There’s no problem when you don’t have to handle HAS_MANY related model(s) with the main model in a single form, no matter how many models might be involved.

But because Order must have many Products, we have to use the tabular input for Products. And we will surely want to add/remove Products on the fly by ajax. As long as my experience tells, there’s a limitation on the ajax validation of the tabular input elements that should be updated by ajax.

I think, first step would be to generate as many models for the connecting table as many rows are visible in the grid. is there a way doing this? somehow the controller should be able to get feedback from ajax…?

second option: to generate as many models as many are in the grid. it would be more simple, but maybe it would kill Yii.

then: show the two ‘arrays’ or columns of models next to each other in a nice way. when selecting the checkbox on the left (from Products), the id of the selected product would become the productId for OrdersProducts. Can it be made somehow? but here is another possible problem: what if the user changes filtering. we should be able keep data in possibly disappearing rows.

it would be a nice future feature to have the ability to somehow create composite models, or to connect models.

based on the Guide article Collecting Tabular Input, I managed to create 10 models and 10 instances of the form.

In fact, it’s not even necessary to have any connection on the form between Products and OrdersProducts. For me it would be fair enough to show them simply next to each other, I mean the grid, and next to it 10 rows of form elements, so visually it would look like they are connected.

is there a way to use the grid’s own auto-incrementing rownumbering? because I can put checkboxes and textfield into the grid, and if I can use a simple autoincrementing value to assign to the names and id-s of these input elements, I guess I would be able to do the trick.

CCheckBoxColumn is not good, because it’s not possible to name it appropriately for this scenario. instead I’m using:




array(

    'value' => 'CHtml::checkBox("OrdersProducts[$row][orderId]", null, array("value" => $data->id, "id" => "OrdersProducts_" . $row . "_orderId"))',

    'type' => 'raw',

),

array(

    'value' => 'CHtml::textField("OrdersProducts[$row][pcs]", null, array("id" => "OrdersProducts_" . $row . "_pcs",))',

    'type' => 'raw',

),



this way, these inputs have exactly the same name and id as if they would be in a normal OrdersProducts form.

one litle notice: ‘normally’ there should be a dropdown for Order (orderId) for all rows, but I just put only one dropdown on the top of the grid, and I guess I will be able to assign the value of this 0th instance of the 10 models to the others’.

I feel myself very close to the solution, but now I’m stuck in the controller.

The submit is done, and I got validation errors in the top of the form. not exactly where I would like to see them, but maybe later I can do something with it. Now it would be more important not to get validation error for all models, but only for the ones selected with a checkbox.

I’m trying to customize this piece of code, with not much success. I feel it should be not too complicated but I’m only a beginner:




public function actionBatchUpdate()

{

    // retrieve items to be updated in a batch mode

    // assuming each item is of model class 'Item'

    $items=$this->getItemsToUpdate();

    if(isset($_POST['Item']))

    {

        $valid=true;

        foreach($items as $i=>$item)

        {

            if(isset($_POST['Item'][$i]))

                $item->attributes=$_POST['Item'][$i];

            $valid=$item->validate() && $valid;

        }

        if($valid)  // all items are valid

            // ...do something here

    }

    // displays the view to collect tabular input

    $this->render('batchUpdate',array('items'=>$items));

}



any help is greatly appriciated.

Thanks

BR

c

I’m here:




public function actionCreateMultiple() {

    $order = new Order;

    for ($i = 0; $i < 10; $i++) {

        $orderProducts[$i] = new OrderProduct;

    }


    if (isset($_POST['OrderProducts'])) {

        $valid = true;

        foreach ($orderProducts as $i => $orderProduct) {

            if (isset($_POST['OrdersProducts'][$i]['orderId'])) { // we only deal with rows checked

                $orderProduct->attributes = $_POST['OrderProducts'][$i];

                $orderProduct->productId = $_POST['OrderProducts'][0]['productId']; // I give all models' the productId of the one and only productId dropdown.

            } else {

                unset($orderProducts[$i]);

            }

            $valid = $ordersProducts->validate() && $valid;

        }

        if ($valid) {// all orderProducts are valid

            // ...do something here

        }

    }


    $this->render('createMultiple', array('order' => $order, 'orderProducts' => $orderProducts));

}



now it works, with a few flaws:

  • validation errors are still on the top,

  • users have to know, no matter if they’ve put any amounts into any texfields, only those rows will be evaluated, where a checkbox is checked.

  • a good upgrade would be if checking the checkbox would fill the textfield with the value of an other attribute, but that’s js and I’m not at home in js (too).

  • one more thing is missing, to keep checked states and typed amounts in textfields.

If somebody helps me with this, I would be grateful.

Thanks.

BR

c

one thing seems to be solved, to keep selected checkboxes and typed numbers:




array(

    'value' => 'CHtml::checkBox("OrdersProducts[$row][orderId]", $_POST["OrdersProducts"][$row]["orderId"], array("value" => $data->id, "id" => "OrdersProducts_" . $row . "_orderId"))',

    'type' => 'raw',

),

array(

    'value' => 'CHtml::textField("OrdersProducts[$row][pcs]", $_POST["OrdersProducts"][$row]["pcs"], array("id" => "OrdersProducts_" . $row . "_pcs",))',

    'type' => 'raw',

),



maybe it’s not the most elegant solution, so if somebody knows a better one, it’s very welcome.

I have a problem now, filtering is not working in the grid. and I don’t really see why. it should work. if I use the same as I used in this thread, there is ajax loading, in firebug request seems OK, but no filtering happens, there is not even an error message, but if I change it back to textField (so, remove listDataEx), I got the following error message: in firebug there is a GET call in red for a second, and after that:




Fatal error: Call to a member function isAttributeRequired() on a non-object in

...\framework\web\helpers\CHtml.php on line 1237



I have searched for this error, and found this thread that seemed relevant but as I see I have assigned value to $model ($order).

controller:




public function actionCreateMultiple() {

    $order = new Order('search');

    $order ->unsetAttributes();


    for ($i = 0; $i < 10; $i++) {

        $orderProducts[$i] = new OrderProduct;

    }


    if (isset($_POST['OrderProducts'])) {

        $valid = true;

        foreach ($orderProducts as $i => $orderProduct) {

            if (isset($_POST['OrdersProducts'][$i]['orderId'])) { // we only deal with rows checked

                $orderProduct->attributes = $_POST['OrderProducts'][$i];

                $orderProduct->productId = $_POST['OrderProducts'][0]['productId']; // I give all models' the productId of the one and only productId dropdown.

            } else {

                unset($orderProducts[$i]);

            }

            $valid = $ordersProducts->validate() && $valid;

        }

        if ($valid) {// all orderProducts are valid

            // ...do something here

        }

    }


    $this->render('createMultiple', array('order' => $order, 'orderProducts' => $orderProducts));

}



createMultiple.php:




<?php

$this->renderPartial('_formMultiple', array(

    'product' => $product,

    'orderProducts' => $orderProducts,

    'buttons' => 'create'));

?>



form:




<div class="form">

    <?php

    $form = $this->beginWidget('GxActiveForm', array(

        'id' => 'product-form',

        'enableAjaxValidation' => false,

            ));

    ?>


    <p class="note">

        <?php echo Yii::t('app', 'Fields with'); ?> <span class="required">*</span> <?php echo Yii::t('app', 'are required'); ?>.

    </p>

    <?php

    foreach ($orderProducts as $i => $orderProduct) {

        echo $form->errorSummary($orderProduct);

    }

    ?>


    <div class="row">

        <?php echo $form->labelEx($orderProduct, "[0]orderId"); ?>

        <?php echo $form->dropDownList($orderProduct, "[0]orderId", GxHtml::listDataEx(Order::model()->findAllAttributes(null, true)), array('style' => 'width: auto', 'prompt' => '')); ?>

        <?php echo $form->error($orderProduct, "[0]orderId"); ?>

    </div><!-- row -->


    <?php

    $this->widget('bootstrap.widgets.TbGridView', array(

        'id' => 'product-grid',

        'type' => 'striped bordered condensed',

        'dataProvider' => $product->search(),

        'filter' => $product,

        'template' => "{items}{pager}",

        'columns' => array(

            ...

            'Pos',

            array(

                'name' => 'Pos',

                'filter' => GxHtml::listDataEx(Product::model()->findAllAttributes('Pos', false, array('order' => 'CAST(Pos AS SIGNED)')), 'Pos', 'Pos'),

            array(

                'class' => 'bootstrap.widgets.TbButtonColumn',

                'htmlOptions' => array('style' => 'white-space: nowrap'),

            ),

        ),

    ));


    echo GxHtml::submitButton(Yii::t('app', 'Save'));

    $this->endWidget();

    ?>

</div>



model:




public function search() {

    $criteria = new CDbCriteria;


    ...

    $criteria->compare('Pos', $this->Pos, true);

    ...


    return new CActiveDataProvider($this, array(

                'criteria' => $criteria,

            ));

}



What can be wrong?

remaining flaws:

  • validation errors are still on the top (I would like to make background color red for textField with error)

  • a good upgrade would be if checking the checkbox would fill the textfield with the value of an other attribute, but that’s js and I’m not at home in js (too).

Thanks

BR

c

IT WORKS!

only that was missing:




if (isset($_GET['Product']))

    $rendeles->setAttributes($_GET['Product']);



the great thing is, that filtering goes with GET and submitting the form goes with POST and it’s easy to separate variable names also, depending on what you want to do. the form itself goes with id OrderProduct, and variables also go with this, so you can easily save models in


if (isset($_POST['OrderProduct'])) {...

at the same time, in the same controller. and filtering of Products go with grid id Product. brilliant!

Yii is an awesome framework! I have to confess, I was sure I will not be able to solve this (not becasue of Yii, but because of me and because I could find relevant or similar solution nowhere and nobody could really point me to the right direction), but now it works!

now only small flaws remained to be solved.

BR

c




'filter' => GxHtml::listDataEx(Product::model()->findAllAttributes('Pos', false, array('order' => 'CAST(Pos AS SIGNED)')), 'Pos', 'Pos'),



this makes loading of the page incredibly slow. I had 10 different filters like this, and it took almost 1 minute to load the page. If I take them out, page loading is 2 seconds.