Yii search from function - search

I have a CGridView and I want to search in it. The thing is that I have a column that is modified by a function in my model. Everything start in the search.php view that contains a cgridview that looks like this:
$this->widget('zii.widgets.grid.CGridView', array(
'id'=>'grid-demande',
'summaryText'=>'',
'dataProvider'=>$model->search(),
//'filter'=>$model,
'cssFile'=>Yii::app()->request->baseUrl."/css/my_gridview.css",
'columns'=>array
(
array(
'name'=>'id_post',
'htmlOptions'=>array('width'=>'16%'),
),
array(
'name'=>'fk_authorid',
'htmlOptions'=>array('width'=>'16%'),
'value'=>array($this,'renderNameDmd'),
),
)
As you can see the function renderNameDmd is called to render the name of the author. This function is in my model but is called from the controller:
protected function renderNameDmd($data,$row)
{
$model=$this->loadModelDmd($data->id_post);
return $model->getChAuthor();
}
And in the model class I call:
public function getChAuthor(){
$modelUsr=TUsers::model()->findByPk($this->fk_authorid);
return $this->fk_authorid.', '.$modelUsr->ch_completeName;
}
Everything works fine for displaying. My main problem is that I want to search through this cgridview and I can't search with the values that are displayed. Here is my search function that is contained in my model:
public function search()
{
// Warning: Please modify the following code to remove attributes that
// should not be searched.
$criteria=new CDbCriteria;
//more criterias
$criteria->compare('fk_cip',$this->fk_cip,true);
return new CActiveDataProvider($this, array(
'criteria'=>$criteria,
));
}
For the moment, I have tried a couple of things but nothing worked so I reseted the code to what it was initialy. For now, if I search through my cgridview I can only filter with the authorid and not the full column format I wrote. Exemple:
For a row that looks like this:
3231, John Doe
I can only search through the:
3231
I want to search through the row I created from the function.
Thank you for your help!

Well, Yii is quite handy for this kind of feature, but you should first rewrite your model to use relations.
In your model :
// this attribute will be used in search function
private $_authorName;
public function rules()
{
return array (
.....
array('authorName', 'safe', 'on'=>'search'),
.....
);
}
public function relations()
{
return array(
.....
'author' => array(self::BELONGS_TO, 'TUsers', 'fk_authorid'),
.....
);
}
// authorName getter
public function getAuthorName()
{
if ($this->scenario=='search')
return $this->_authorName;
return $this->fk_authorid.', '.$this->author->ch_completeName;
}
// authorName setter
public function setAuthorName($authorName) {
$this->_authorName = $authorName;
}
public function search()
{
$criteria=new CDbCriteria;
.....
// search author name ?
if ($this->authorName!==null)
{
$criteria->with = array('author');
$criteria->compare('author.ch_completeName', $this->authorName, true);
}
.....
return new CActiveDataProvider($this, array(
'criteria'=>$criteria,
));
}
And in your CGridView, you should simply define your column like this :
array(
'name'=>'authorName',
'htmlOptions'=>array('width'=>'16%'),
),
And you should read this :
http://www.yiiframework.com/wiki/281/searching-and-sorting-by-related-model-in-cgridview/

Related

Codeigniter redirect()->to() doesnt work in __construct()

I have code in my controller
public function __construct()
{
return redirect()->to('/auth');
$this->validation =
\Config\Services::validation();
$this->title = 'Header Menu';
$this->controller = 'user';
}
public function index()
{
$data = [
'title_prefix' => 'Profil',
'title' => $this->title,
'controller' => $this->controller,
'button_add' => false,
'validation' => $this->validation
];
return view('user/index', $data);
}
it still show view('user/index'). How to get to return redirect()->to('/auth') in __construct() ?
sorry i'm not speaking english well
It is an expected behavior that redirect() doesn't work inside a constructor.
redirect() in CI4 doesn't just set headers but return a RedirectResponse object.
Problem is : while being in the constructor of your controller, you can't return an instance of something else. You're trying to construct a Controller not a RedirectResponse.
Good practice is to use Controller Filters
Or you could add the redirect() inside your index function if there's only at this endpoint that you would like to redirect the user.
Here's an example of the filter that would fit your need :
Watch out your CI version. The parameter $arguments is needed since 4.0.4. Otherwise you have to remove it
<?php
namespace App\Filters;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Filters\FilterInterface;
class AuthFilter implements FilterInterface {
public function before(RequestInterface $request, $arguments = null) {
return redirect()->to('/auth');
}
//--------------------------------------------------------------------
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) {
}
}
And in your app/Config/Filters edit those 2 variables in order to activate your filter :
public $aliases = [
'auth' => \CodeIgniter\Filters\AuthFilter::class,
];
public $globals = [
'before' => ['auth' => 'the_routes_you_want_to_redirect']
];
You might want to check this thread aswell : https://forum.codeigniter.com/thread-74537.html
you can't use redirect() inside __construct() function or initController() directly.
But you can use $response parameter in initController() or $this->response attribute.
https://stackoverflow.com/a/65814413/1462903
in controller class
<?php namespace App\Controllers;
class Data extends BaseController
{
public function initController(\CodeIgniter\HTTP\RequestInterface $request, \CodeIgniter\HTTP\ResponseInterface $response, \Psr\Log\LoggerInterface $logger)
{
// Do Not Edit This Line
parent::initController($request, $response, $logger);
if($this->session->get('is_loggedin') !== true){
$response->redirect(base_url('login')); // or use $this->response->redirect(base_url('login'));
}
}
public function index()
{
return view('dashboard');
}
}
in BaseController class
/**
* Constructor.
*/
public function initController(\CodeIgniter\HTTP\RequestInterface $request, \CodeIgniter\HTTP\ResponseInterface $response, \Psr\Log\LoggerInterface $logger)
{
// Do Not Edit This Line
parent::initController($request, $response, $logger);
//--------------------------------------------------------------------
// Preload any models, libraries, etc, here.
//--------------------------------------------------------------------
$this->session = \Config\Services::session();
}
header("Location: ".site_url().'/dashboard');
die();

How to efficiently return multiple DriverResults from the Display method?

This article describes how to write efficient DisplayDrivers for your Parts so that expensive code is only executed when the shape is actually displayed.
protected override DriverResult Display(MyPart part, string displayType, dynamic shapeHelper)
{
// return the shape
return ContentShape("Parts_MyPart", () => {
// do computations here
return shapeHelper.Parts_MyPart();
});
}
Now I'd like to make a Part that returns multiple DriverResults using the Combine method, with each DriverResult containing mostly the same data, which is fetched from the database. The problem is I can't think of a good way to make it efficient, since Combine doesn't take a Func parameter.
protected override DriverResult Display(MyPart part, string displayType, dynamic shapeHelper)
{
var data = ... // expensive query specific to the part
return Combined(
ContentShape("Parts_MyPart_A", () => shapeHelper.Parts_MyPart_A(
Data: data
)),
ContentShape("Parts_MyPart_B", () => shapeHelper.Parts_MyPart_B(
Data: data
)),
ContentShape("Pars_MyPart_C", ...
);
}
Can I achieve the same result so that the query is not executed if nothing is displayed and only executed once if multiple shapes are displayed?
I want to do this so I can display the same data on a ContentItem's Detail in different zones with different markup and styling. An alternative approach could be to return one shape that in turn pushes other shapes into different zones but then I would lose the ability to use Placement to control each of them individually.
I'd probably add a lazy field to your Part.
public class MyPart : ContentPart {
internal readonly LazyField<CustomData> CustomDataField = new LazyField<CustomData>();
public CustomData CustomData {
get { return CustomDataField.Value; }
}
}
public class CustomData {
...
}
public class MyPartHandler : ContentPartHandler {
private ICustomService _customService;
public MyPartHandler(ICustomService customService){
_customService = customService;
OnActivated<MyPart>(Initialize);
}
private void Initialize(ActivatedContentContext context, MyPart part){
part.CustomDataField.Loader(() => {
return _customService.Get(part.ContentItem.Id);
});
}
}
It will only be calculated if it is loaded and all the shapes will share the calculated value.

bind object to more than one fieldset in a form

Having one entity but two fieldsets, I found out that just one fieldset is populated when using the use_as_base_fieldset option.
Is there a solution for that problem?
First fieldset
class ProfileFieldset extends AbstractFieldset implements InputFilterProviderInterface
{
public function __construct($em)
{
....
$this->setHydrator(new Hydrator(false))
->setObject(new User());
...
}
...
}
Second Fieldset
<?php
use \Zend\InputFilter\InputFilterProviderInterface;
use Zend\Stdlib\Hydrator\ClassMethods as Hydrator;
use User\Entity\User;
class CredentialsFieldset extends AbstractFieldset
implements InputFilterProviderInterface
{
public function __construct($em)
{
.....
$this->setHydrator(new Hydrator(false))
->setObject(new User());
....
}
...
}
Form
use Zend\Stdlib\Hydrator\ClassMethods as Hydrator;
use User\Entity\User;
class UserForm extends AbstractForm
{
public function __construct()
{
....
$this->setHydrator(new Hydrator(false))
->setObject(new User());
....
$this->add(array(
'type' => 'User\Form\Fieldset\ProfileFieldset',
'options' => array(
'use_as_base_fieldset' => true
)
));
$this->add(array(
'type' => 'User\Form\Fieldset\CredentialsFieldset',
'options' => array(
'use_as_base_fieldset' => true
)
));
....
}
The entity itself contains properties for both fieldsets...
When binding the user entity for editing, just the last added fieldset is populated. Of course, there can be only one base fieldset ...
Anyone having an idea, how to solve that problem?
I think this could be done by creating a new fieldset that consists of the two fieldsets.
class UserForm extends AbstractForm
{
public function __construct()
{
$this->add(array(
'type' => 'User\Form\Fieldset\UserFieldset',
'options' => array(
'use_as_base_fieldset' => true
)
));
....
}
Then UserFieldset would become
<?php
namespace User\Form\Fieldset;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;
use Zend\Stdlib\Hydrator\ClassMethods as ClassMethodsHydrator;
class UserFieldset extends Fieldset {
public function __construct()
{
parent::__construct('user');
....
$this->add(array(
'type' => 'User\Form\Fieldset\ProfileFieldset',
));
$this->add(array(
'type' => 'User\Form\Fieldset\CredentialsFieldset',
));
.....
}
public function init()
{
$this->setHydrator(new ClassMethodsHydrator(false))
->setObject(new User());
....
}
....
}

Form: Avoid setting null to non submitted field

I've got a simple model (simplified of source):
class Collection
{
public $page;
public $limit;
}
And a form type:
class CollectionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('page', 'integer');
$builder->add('limit', 'integer');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'FSC\Common\Rest\Form\Model\Collection',
));
}
}
My controller:
public function getUsersAction(Request $request)
{
$collection = new Collection();
$collection->page = 1;
$collection->limit = 10;
$form = $this->createForm(new CollectionType(), $collection)
$form->bind($request);
print_r($collection);exit;
}
When i POST /users/?form[page]=2&form[limit]=20, the response is what i expect:
Collection Object
(
[page:public] => 2
[limit:public] => 20
)
Now, when i POST /users/?form[page]=3, the response is:
Collection Object
(
[page:public] => 3
[limit:public] =>
)
limit becomes null, because it was not submitted.
I wanted to get
Collection Object
(
[page:public] => 3
[limit:public] => 10 // The default value, set before the bind
)
Question: How can i change the form behaviour, so that it ignores non submitted values ?
If is only a problem of parameters (GET parameters) you can define the default value into routing file
route_name:
pattern: /users/?form[page]={page}&form[limit]={limit}
defaults: { _controller: CompanyNameBundleName:ControllerName:ActionName,
limit:10 }
An alternative way could be to use a hook (i.e. PRE_BIND) and update manually that value into this event. In that way you haven't the "logic" spreaded into multi pieces of code.
Final code - suggested by Adrien - will be
<?php
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;
class IgnoreNonSubmittedFieldSubscriber implements EventSubscriberInterface
{
private $factory;
public function __construct(FormFactoryInterface $factory)
{
$this->factory = $factory;
}
public static function getSubscribedEvents()
{
return array(FormEvents::PRE_BIND => 'preBind');
}
public function preBind(FormEvent $event)
{
$submittedData = $event->getData();
$form = $event->getForm();
// We remove every child that has no data to bind, to avoid "overriding" the form default data
foreach ($form->all() as $name => $child) {
if (!isset($submittedData[$name])) {
$form->remove($name);
}
}
}
}
Here's a modification of the original answer. The most important benefit of this solution is that validators can now behave as if the form post would always be complete, which means there's no problems with error bubbling and such.
Note that object field names must be identical to form field names for this code to work.
<?php
namespace Acme\DemoBundle\Form;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;
class FillNonSubmittedFieldsWithDefaultsSubscriber implements EventSubscriberInterface
{
private $factory;
public function __construct(FormFactoryInterface $factory)
{
$this->factory = $factory;
}
public static function getSubscribedEvents()
{
return array(FormEvents::PRE_BIND => 'preBind');
}
public function preBind(FormEvent $event)
{
$submittedData = $event->getData();
$form = $event->getForm();
// We complete partial submitted data by inserting default values from object
foreach ($form->all() as $name => $child) {
if (!isset($submittedData[$name])) {
$obj = $form->getData();
$getter = "get".ucfirst($name);
$submittedData[$name] = $obj->$getter();
}
}
$event->setData($submittedData);
}
}

Create content error - Specified cast is not valid

I have a custom module. Migrations.cs looks like this.
public int Create()
{
SchemaBuilder.CreateTable("MyModuleRecord", table => table
.ContentPartRecord()
...
);
ContentDefinitionManager.AlterPartDefinition(
typeof(MyModulePart).Name, cfg => cfg.Attachable());
ContentDefinitionManager.AlterTypeDefinition("MyModule",
cfg => cfg
.WithPart("MyModulePart")
.WithPart("CommonPart")
.Creatable()
);
return 1;
}
This is the code I have in the controller.
var newcontent = _orchardServices.ContentManager.New<MyModulePart>("MyModule");
...
_orchardServices.ContentManager.Create(newcontent);
I get the invalid cast error from this New method in Orchard.ContentManagement ContentCreateExtensions.
public static T New<T>(this IContentManager manager, string contentType) where T : class, IContent {
var contentItem = manager.New(contentType);
if (contentItem == null)
return null;
var part = contentItem.Get<T>();
if (part == null)
throw new InvalidCastException();
return part;
}
Any idea what I am doing wrong?
Thanks.
This is the handler.
public class MyModuleHandler : ContentHandler
{
public MyModuleHandler(IRepository<MyModuleRecord> repository)
{
Filters.Add(StorageFilter.For(repository));
}
}
You are getting the InvalidCastException because the content item doesn't appear to have your MyModulePart attached.
If there were a driver for your part, then there is an implicit link somewhere that allows your part to be shown on a content item (I'm not sure how this is done, maybe someone else could elaborate - but it is something to do with how shapes are harvested and picked up by the shape table deep down in Orchard's core).
However since you don't have a driver, adding an ActivatingFilter to your part's handler class will make the link explicitly:
public MyModulePartHandler : ContentHandler {
public MyModulePartHandler() {
Filters.Add(StorageFilter.For(repository));
Filters.Add(new ActivatingFilter<MyModulePart>("MyModule");
}
}
Your part table name is wrong. Try renaming it to this (so the part before "Record" matches your part model name exactly):
SchemaBuilder.CreateTable("MyModulePartRecord", table => table
.ContentPartRecord()
...
);

Resources