I have ArrayDataProvider with field of type DateTime - birthdate.
I use that dataprovider for a gridview in view section.
Since birthdates include the birthyear the sorting is not working as expected.
Is there a way to somehow tell the sorting mechanism not to account years and only sort by month and day ? A custom sort function perhaps ?
Edit: Something like sort in C++ where you can pass the compare function.
Edited current solution description:
My solution currently is to include a birthyear as a separate field in array and birthdate's years are set to current year. Now dates are from the same year and the sorting works. But separate field for birth year doesn't feel right. I wish i could get all necessary data from one date object.
This separate field grinds my gears. It's not perfect.
Edit:
Oh, and its Yii2
Update
There are also a PHP array sorting functions that take a compare callback - for example uasort that can be used on associative arrays like mine:
uasort($persons, function($a, $b){
return strcasecmp( $b['date']->format('m-d'), $a['date']->format('m-d') );
});
Now i need to find the way to implement it into ArrayDataProvider. Any ideas here ?
If you have an array dataprovider you can not use sorting of the database of course.
Solution using Yii
To allow sorting by some expression you have to calculate the value beforehand and configure the sorting for the attribute to use the calculated value instead of the original one:
// assuming $data contains your data and 'birthdate' is the value you want to use for sorting
foreach($data as $key => $value) {
$data[$key]['sort_birthdate'] = date('m-d', strtotime($value['birthdate']));
}
$dataProvider = new \yii\data\ArrayDataProvider([
'allModels' => $data,
'sort' => [
'attributes' => [
'birthdate' => [
'asc' => [
'sort_birthdate' => SORT_ASC,
],
'desc' => [
'sort_birthdate' => SORT_DESC,
],
'label' => 'Date',
'default' => SORT_ASC
],
// list all other attributes here
],
]
]);
Solution extending Yii
If you want a custom comparison function, you have to extend the Yii class to support this.
You can create a custom ArrayDataProvider class which extends from the one that comes with Yii and override the sortModels()-method.
You should try something like
use yii\db\Expression;
$dataProvider->setSort([
'attributes' => [
................................
'birthday' => [
'asc' => [
new Expression('DATE_FORMAT(birthday, "%m%d")') => SORT_ASC,
],
'desc' => [
new Expression('DATE_FORMAT(birthday, "%m%d")') => SORT_DESC,
],
'label' => 'Birthday',
'default' => SORT_ASC
],
................................
]
]);
I have not tried this.
What works for sure is to do something like this
$query->select([
new Expression('DATE_FORMAT(birthday, "%m%d") as show_date'),
.................................
]);
Then
/**
* Setup your sorting attributes
* Note: This is setup before the $this->load($params)
* statement below
*/
$dataProvider->setSort([
'attributes' => [
'id',
'show_date' => [
'asc' => [
'sort_date' => SORT_ASC,
],
'desc' => [
'sort_date' => SORT_DESC,
],
'label' => 'Date',
'default' => SORT_ASC
],
'price',
'gst',
'cost',
'quantity',
'profit',
]
]);
Related
I use yii2 and I want to make gridview column 'description' sortable in a case insensitive way. There is my code:
$dataProvider = new ArrayDataProvider([
'allModels' => $query->find(),
'sort' => [
'attributes' => ['name','description],
],
'pagination' => [
'pageSize' => $this->pageSize,
],
]);
When I click on column description to sort, it show like this:
Job Title
Doctor
Teacher
doctor
teacher
As you see it sort case sensitive I want to sort case Insensitive, how I can do that? Any idea?
In order to sort rows of ArrayDataProvider in a case-insensitive way you should extend ArrayDataProvider itself, because internally it uses ArrayHelper::multisort and if you want it sort the way you want you have to pass SORT_STRING | SORT_FLAG_CASE as fourth argument to the method. By default its value equal to SORT_REGULAR constant.
Here the implementation:
<?php
namespace app\dataproviders;
use yii\helpers\ArrayHelper;
/**
* Class ArrayDataProvider
*/
class ArrayDataProvider extends \yii\data\ArrayDataProvider
{
/** #inheritdoc */
protected function sortModels($models, $sort)
{
$orders = $sort->getOrders();
if (!empty($orders)) {
ArrayHelper::multisort(
$models,
array_keys($orders),
array_values($orders),
SORT_STRING | SORT_FLAG_CASE
);
}
return $models;
}
}
And after it use the extended class instead of \yii\data\ArrayDataProvider
Example of usage:
$dataProvider = \app\dataproviders\ArrayDataProvider([
'allModels' => $query->find(),
'sort' => [
'attributes' => ['name','description'],
],
'pagination' => [
'pageSize' => $this->pageSize,
],
]);
That is quite good what Akmal wrote!
Maybe you need to specify key to the ArrayDataProvider, to get right id if your actionColumn not working well!
$dataProvider = \app\dataproviders\ArrayDataProvider([
'allModels' => $query->find(),
'key'=> 'id',
And if you want to filter your data within your gridview, you can use this solution:
Yii2 GridView with ArrrayDataProvider search
I tried this also and it works!!
(And the last tip: if you want to filter case intensive way, you can use stripos instead of strpos).
Good luck!
I have an issue with a Gridview using kartik\grid\EditableColumn, after changing the value I am returned the wrong value for the column when it updates. I am returned the dropdown key/main table integer rather than the string contained in a linked table.
I have two tables
Leads - columns id and status_id
Related fields - model, field, related_value, related_value
The relation is based on in this case
model:"Leads",
field:"status_id",
related_id:status_id
I have the following relation in my model
public function getStatus()
{
return $this->hasOne(RelatedFields::className(), ["related_id" => "status_id"])->andOnCondition(["status.field" => "status_id", "status.model"=>"Leads"])->from(["status" => RelatedFields::tableName()]);
}
I also created the following as a test based on this link
public function getStatusValue()
{
return $this->status->related_value;
}
Here is the column code
[
'class' => 'kartik\grid\EditableColumn',
'attribute' => 'status_id',
'value'=>'status.related_value',
//'value' => function($model){ return $model->status->related_value; },
//'value' => function($model){ return $model->StatusValue; },
//'refreshGrid' => true,//Works but not nice
'vAlign'=>'middle',
'hAlign'=>'center',
'pageSummary' => true,
'readonly' => false,
'width'=>'10%',
'filter'=>Html::activeDropDownList($searchModel, 'status', ArrayHelper::map(RelatedFields::Find()->where(['model' =>"Leads","field"=>"status_id"])->all(), 'related_id', 'related_value'),['class' => 'form-control','prompt' => Yii::t('app', '')]),
'editableOptions'=> [
//'attribute'=>'status_id',
//'value'=>'status.related_value',
//'header' => 'profile',
//'format' => Editable::FORMAT_BUTTON,
'inputType' => Editable::INPUT_DROPDOWN_LIST,
'data'=> ArrayHelper::map(RelatedFields::Find()->where(['model' =>"Leads","field"=>"status_id"])->all(), 'related_id', 'related_value'),
]
],
Commented out are a number of lines in my attempts to fix the issue as well as combinations of them, however all result in the wrong value.
If for example I select the related value "New" which has a related_id 1, after the column has been updated I get the value 1 instead of "New".
When the table is first loaded/reloaded the value does show correctly.
I could reload the grid, but this seems wrong just to fix 1% of the data shown on the page.
I your model take a public variable $status_value
create an assigning value method
public function getStatusValue()(){
return $this->status_value= $this->status->related_value;
}
Now in Gridview use getStatusValueenter code heremethod with assigning value as below
use yii\helpers\Url;
$gridColumns = [
[
'class' => 'kartik\grid\EditableColumn',
'attribute' => 'status_value',
'pageSummary' => true,
'readonly' => false,
'value' => function($model){ return $model->statusValue; }, // assign value from getStatusValue method
'editableOptions' => [
'header' => 'status_value',
'inputType' => kartik\editable\Editable::INPUT_TEXT,
'options' => [
'pluginOptions' => [
]
]
],
],
];
If you follow Kartik guide, he suggest to add EditableColumnAction to better handle the editable column:
The EditableColumnAction offers a quick easy way to setup your
controller action for updating, saving and managing the EditableColumn
output from GridView. This action class extends from yii\rest\Action
and hence all properties available with yii\rest\Action are applicable
here. The basic setup of the column involves setting up the controller
action and the EditableColumn.
So you need to add an EditableColumnAction in your controller to handle the update of the model:
public function actions()
{
return ArrayHelper::merge(parent::actions(), [
'edit-lead' => [
'class' => EditableColumnAction::class,
'modelClass' => Leads::class
]
]);
}
In your GridView editable column configuration, include the above
controller action for processing the Editable within editableOptions.
For example
And in your column code you need to add the action to editableOptions property:
'editableOptions' => [
...
'formOptions' => ['action' => ['/leads/edit-lead']]
]
Now, according to the guide, you can add to your action the outputValue property:
'outputValue' => function (Leads $model) {
return $model->status->related_value;
}
I made custom currency formatter + converter based on values from database.
This is how I use it in DetailView
use yii\helpers\Html;
use app\commands\AppHelper;
use yii\widgets\DetailView;
use app\models\Part;
<?= DetailView::widget([
'model' => $model,
'attributes' => [
// ...
[
'attribute' => 'price',
'label' => (new Part())->getAttributeLabel('price_user'),
'format' => [
'currency',
AppHelper::getUserCurrencyCode(),
[
'convert' => true,
'currencyFrom' => $model->currency->code,
'currencyTo' => AppHelper::getUserCurrencyCode(),
],
],
],
// ...
],
]) ?>
In this widget I can accomplish behaviour like this: when there is numeric value, it gets formatted, if there is NULL value, usual (not-set) is printed out...
Notice $model->currency->code which is data from relation, in DetailView easily accessible but I can not figure out how to get that data into formatter in GridView.
Problem is when I want to format data in GridView.
I allow NULL values on column that I need to use formatter on, so I already threw away idea of using
'value' => function ($data, $key, $index, $column) { return $data->value; }
because when NULL value is present, yii sends data like this
<span class="not-set">(not set)</span>
and either I want to let it be or set my custom value (considering different value for other columns with NULL value) and I also want to save trouble handling all those (not set) values.
Another reason is, as I noticed, that if I use 'format' => ... in attribute params, formatting happens before setting those (not set) values.
So I was thinking about somehow passing that $model->currency->code, which is data from relation, to that formatter.
Any ideas? Thanks.
Worst case scenario I will use formatter in value dumping values that contains '<span' or NULL like this, but it is ugly and I dont like it...
EDIT: I added custom static method to format unset data. I still dont like it, but hey, it works ... :D
use yii\helpers\Html;
use app\commands\AppHelper;
use yii\grid\GridView;
use app\models\Part;
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
// ...
[
'attribute' => 'price',
'label' => (new Part())->getAttributeLabel('price_user'),
'value' => function ($data, $key, $index, $column) {
return Part::requestPrice(Yii::$app->formatter->asCurrency(
$data->price,
AppHelper::getUserCurrencyCode(),
[
'precision' => 2,
'convert' => true,
'currencyFrom' => $data->currencyCode,
'currencyTo' => AppHelper::getUserCurrencyCode(),
]));
},
'format' => 'raw',
],
// ...
],
]); ?>
and in Part.php (Part model) I added method
public static function requestPrice($price)
{
if (strpos($price, 'class') !== false || empty($price) || floatval($price) == 0)
return '<span class="not-set">' . Yii::t('app', 'na vyĹžiadanie') . '</span>';
else
return $price;
}
I have a timestamp column, and I simply want to format it. In GridView I have the following:
[
'attribute' => 'timestamp',
'filter' => false,
'value' => function($model, $key, $index, $column) {
// How to get current timestamp value here???
}
],
Documentation says, $model and $column both return objects, but I still could not find methods that provide column's data. How would I go about this?
You can call any attribute through getter in closure using $model and that will return attribute for current model (according to row in GridView):
[
'attribute' => 'timestamp',
'filter' => false,
'value' => function($model, $key, $index, $column){
return $model->timestamp;
}
],
Obviously such return doesn't make any sense, but you can format it somehow you want. There are some built-in options for date / datetime formatting in Yii2, you can check them in official docs here:
Formatter asDate()
Formatter asDateTime()
I use the following with Yii 1:
[
'attribute' => 'timestamp',
'filter' => false,
'value' => 'Custom::formatDateTime($data->date_stamp)',
],
Obviously formatDateTime is a custom method I created.
In my example code I am using the php client library, but it should be understood by anyone familiar with elasticsearch.
I'm using elasticsearch to create an index where each document contains an array of nGram indexed authors. Initially, the document will have a single author, but as time progresses, more authors will be appended to the array. Ideally, a search could be executed by an author's name, and if any of the authors in the array get matched, the document will be found.
I have been trying to use the documentation here for appending to the array and here for using the array type - but I have not had success getting this working.
First, I want to create an index for documents, with a title, array of authors, and an array of comments.
$client = new Client();
$params = [
'index' => 'document',
'body' => [
'settings' => [
// Simple settings for now, single shard
'number_of_shards' => 1,
'number_of_replicas' => 0,
'analysis' => [
'filter' => [
'shingle' => [
'type' => 'shingle'
]
],
'analyzer' => [
'my_ngram_analyzer' => [
'tokenizer' => 'my_ngram_tokenizer',
'filter' => 'lowercase',
]
],
// Allow searching for partial names with nGram
'tokenizer' => [
'my_ngram_tokenizer' => [
'type' => 'nGram',
'min_gram' => 1,
'max_gram' => 15,
'token_chars' => ['letter', 'digit']
]
]
]
],
'mappings' => [
'_default_' => [
'properties' => [
'document_id' => [
'type' => 'string',
'index' => 'not_analyzed',
],
// The name, email, or other info related to the person
'title' => [
'type' => 'string',
'analyzer' => 'my_ngram_analyzer',
'term_vector' => 'yes',
'copy_to' => 'combined'
],
'authors' => [
'type' => 'list',
'analyzer' => 'my_ngram_analyzer',
'term_vector' => 'yes',
'copy_to' => 'combined'
],
'comments' => [
'type' => 'list',
'analyzer' => 'my_ngram_analyzer',
'term_vector' => 'yes',
'copy_to' => 'combined'
],
]
],
]
]
];
// Create index `person` with ngram indexing
$client->indices()->create($params);
Off the get go, I can't even create the index due to this error:
{"error":"MapperParsingException[mapping [_default_]]; nested: MapperParsingException[No handler for type [list] declared on field [authors]]; ","status":400}
HAD this gone successfully though, I would plan to create an index, starting with empty arrays for authors and title, something like this:
$client = new Client();
$params = array();
$params['body'] = array('document_id' => 'id_here', 'title' => 'my_title', 'authors' => [], 'comments' => []);
$params['index'] = 'document';
$params['type'] = 'example_type';
$params['id'] = 'id_here';
$ret = $client->index($params);
return $ret;
This seems like it should work if I had the desired index to add this structure of information to, but what concerns me would be appending something to the array using update. For example,
$client = new Client();
$params = array();
//$params['body'] = array('person_id' => $person_id, 'emails' => [$email]);
$params['index'] = 'document';
$params['type'] = 'example_type';
$params['id'] = 'id_here';
$params['script'] = 'NO IDEA WHAT THIS SCRIPT SHOULD BE TO APPEND TO THE ARRAY';
$ret = $client->update($params);
return $ret;
}
I am not sure how I would go about actually appending a thing to the array and making sure it's indexed.
Finally, another thing that confuses me is how I could search based on any author in the array. Ideally I could do something like this:
But I'm not 100% whether it will work. Maybe there is something fundemental about elasticsearch that I am not understanding. I am completely new to so any resources that will get me to a point where these little details don't hang me up would be appreciated.
Also, any direct advice on how to use elasticsearch to solve these problems would be appreciated.
Sorry for the big wall of text, to recap, I am looking for advice on how to
Create an index that supports nGram analysis on all elements of an array
Updating that index to append to the array
Searching for the now-updated index.
Thanks for any help
EDIT: thanks to #astax, I am now able to create the index and append to the value as a string. HOWEVER, there are two problems with this:
the array is stored as a string value, so a script like
$params['script'] = 'ctx._source.authors += [\'hello\']';
actually appends a STRING with [] rather than an array containing a value.
the value inputted does not appear to be ngram analyzed, so a search like this:
$client = new Client();
$searchParams['index'] = 'document';
$searchParams['type'] = 'example_type';
$searchParams['body']['query']['match']['_all'] = 'hello';
$queryResponse = $client->search($searchParams);
print_r($queryResponse); // SUCCESS
will find the new value but a search like this:
$client = new Client();
$searchParams['index'] = 'document';
$searchParams['type'] = 'example_type';
$searchParams['body']['query']['match']['_all'] = 'hel';
$queryResponse = $client->search($searchParams);
print_r($queryResponse); // NO RESULTS
does not
There is no type "list" in elasticsearch. But you can use "string" field type and store array of values.
....
'comments' => [
'type' => 'string',
'analyzer' => 'my_ngram_analyzer',
'term_vector' => 'yes',
'copy_to' => 'combined'
],
....
And index a document this way:
....
$params['body'] = array(
'document_id' => 'id_here',
'title' => 'my_title',
'authors' => [],
'comments' => ['comment1', 'comment2']);
....
As for the script for apending an element to array, this answer may help you - Elasticsearch upserting and appending to array
However, do you really need to update the document? It might be easier to just reindex it as this is exactly what Elasticsearch does internally. It reads the "_source" property, does the required modification and reindexes it. BTW, this means that "_source" must be enabled and all properties of the document should be included into it.
You also may consider storing comments and authors (as I understand these are authors of comments, not the document authors) as child document in ES and using "has_child" filter.
I can't really give you specific solution, but strongly recommend installing Marvel plugin for ElasticSearch and use its "sense" tool to check how your overall process works step by step.
So check if your tokenizer is properly configured by running tests as described at http://www.elastic.co/guide/en/elasticsearch/reference/1.4/indices-analyze.html.
Then check if your update script is doing what you expect by retrieving the document by running GET /document/example_type/some_existing_id
The authors and comments should be arrays, but not strings.
Finally perform the search:
GET /document/_search
{
'query' : {
'match': { '_all': 'hel' }
}
}
If you're building the query yourself rather than getting it from the user, you may use query_string with placeholders:
GET /document/_search
{
'query' : {
'query_string': {
'fields': '_all',
'query': 'hel*'
}
}
}