I have rewritten the app/code/core/Mage/Adminhtml/Block/Sales/Order/Grid.php
with app/code/local/Mage/Adminhtml/Block/Sales/Order/Grid.php
& have created a renderer to display customer's email column in grid.
Here is my renderer file:
class Mage_Adminhtml_Block_Renderer_Customer extends Mage_Adminhtml_Block_Widget_Grid_Column_Renderer_Abstract
{
public function render(Varien_Object $row)
{
$model = Mage::getModel('customer/customer')->load($row->getCustomerId());
return $model->getEmail();
}
}
& here is my Grid changes (I just added a column, & I intend to make it search-able)
$this->addColumn('billing_name', array(
'header' => Mage::helper('sales')->__('Bill to Name'),
'index' => 'billing_name',
));
// this is new col.
$this->addColumn('customer_email', array(
'header' => Mage::helper('sales')->__('Customer Email'),
'renderer' => 'adminhtml/renderer_customer',
));
I am getting what I want. But this col/ has a lot of whitespace both leading & trailing
due to this I think this col. is not search-able.
Can Anybody suggest what can be done in order to remove these white spaces
Many thanks in advance
EDIT
After few days I have figured out that these white spaces are common in the grid & it has nothing to do with the search-able option.
Can anybody suggest that how to make a custom column in search-able that has been added to a grid by using a renderer ???
Thanks
2 EDIT
Guys According to the clockworkgeek I have customized
my _prepareCollection() method of the overwritten grid as follows
protected function _prepareCollection()
{
// 'sales/order_collection' is changed from 'sales/order_grid_collection'
$collection = Mage::getResourceModel('sales/order_collection');
$collection->addAttributeToSelect('*')
->joinAttribute('billing_firstname', 'order_address/firstname', 'billing_address_id', null, 'left')
->joinAttribute('billing_lastname', 'order_address/lastname', 'billing_address_id', null, 'left')
->joinAttribute('shipping_firstname', 'order_address/firstname', 'shipping_address_id', null, 'left')
->joinAttribute('shipping_lastname', 'order_address/lastname', 'shipping_address_id', null, 'left')
->joinAttribute('billing_fax', 'order_address/fax', 'billing_address_id', null, 'left')
->joinAttribute('billing_telephone', 'order_address/telephone', 'billing_address_id', null, '')
->addExpressionAttributeToSelect('billing_name',
'CONCAT({{billing_firstname}}, " ", {{billing_lastname}})',
array('billing_firstname', 'billing_lastname'))
->addExpressionAttributeToSelect('shipping_name',
'CONCAT({{shipping_firstname}}, " ", {{shipping_lastname}})',
array('shipping_firstname', 'shipping_lastname'));
$this->setCollection($collection);
return parent::_prepareCollection();
}
I also have investigated that for Grid Magento obtains data from sales_flat_order_grid table not from sales_flat_order this is the reason it was reporting error of unknow column as per the clockworkgeek first solution
THe issue with current implementation is Magento reports an error Fatal error: Call to undefined method Mage_Sales_Model_Mysql4_Order_Collection::addExpressionAttributeToSelect()
as Mage_Sales_Model_Mysql4_Order_Collection does not have addExpressionAttributeToSelect method instead it has addExpressionFieldToSelect method
Now I need help to write a proper syntax for addExpressionAttributeToSelect method. Changing the method name only is not helping me. I also have referred the docs
Add 'index' => 'email' to your addColumn() in the Grid.php and then try something like this:
$emailaddress = trim($row->getData($this->getColumn()->getIndex()));
return ''.$emailaddress.'';
That way you strip the whitespace and also provide a clickable link for your admin users :)
In response to the second part of your question may I offer this little trick.
Adminhtml grid columns can take an extra filter_condition_callback parameter which takes a standard PHP callback type. In your case you might modify the grid like this:
protected function _prepareColumns() {
// ...
$this->addColumn('customer_email', array(
'header' => Mage::helper('sales')->__('Customer Email'),
'renderer' => 'adminhtml/renderer_customer',
'filter_condition_callback' => array($this, 'addCustomerEmailFilter'),
));
}
public function addCustomerEmailFilter(Mage_Eav_Model_Entity_Collection_Abstract $collection, Mage_Adminhtml_Block_Widget_Grid_Column $column) {
$collection->addAttributeToFilter('customer_email', $column->getFilter()->getValue());
}
But all that still feels a bit messy, especially if the attribute is not a first class column. For those unusual cases you can combine the output processing and searching in the collection class...
protected function _initSelect() {
parent::_initSelect();
// email is existing column, customer_email is generated column
$this->addExpressionAttributeToSelect(
'customer_email',
'TRIM({{email}})',
array('email')
);
return $this;
}
The addExpressionAttributeToSelect() method temporarily stores the SQL expression as a mapped field so that when a grid tries to search for customer_email it gets substituted by the expression instead.
Related
So, I'm using this extension: x-editable for yii.
And I'm currently trying to update two models in update() function.
I have two models:
Realisasi.php
RealisasiDeadline.php.
So when a cell is updated on table Realisasi.php (one value in column t1701 in this case), I want the function to update the corresponding value in column t1701 of table RealisasiDeadline, using column no as the foreign key.
Since I haven't found any example on Google, I made it up myself:
public function actionSimpanEdit($kode) {
Yii::import('editable.EditableSaver');
$es = new EditableSaver($_GET['model']); // 'modelName' is classname of model to be updated
$es->update();
$es2 = RealisasiDeadline::model()->findByPk($kode);//this is where I'm stuck
$es2['t1701'] = '1991-11-19';//this too
$es->update();//and this
}
This is the view.php:
array(
'name' => 't1701',
'value' => 'CHtml::value($data,"hubtarget.t1701")=== "0"?"Target Nol":$data->t1701',
'header' => 'Bkl Selatan',
'class' => 'editable.EditableColumn',
'editable' => array(
'url' => $this->createUrl('simpanEdit', array('model' => 'Realisasi', 'kode'=>'$data->no')),
)
),
What have I missed? Is it possible at all to do? If not, is there another solution?
UPDATE
It's not showing any error. But the value in table RealisasiDeadline doesn't change, only the one in Realisasi does.
Added some comments to original function so you can improve upon it. Biggest issue with this code is that looking at it I have no idea what it does.
public function actionSimpanEdit($kode) {
Yii::import('editable.EditableSaver'); // Should be at the top of the file
// For the love of god use descriptive variable names
$es = new EditableSaver($_GET['model']); // Would prefer to have model as actions argument
$es->update();
$es2 = RealisasiDeadline::model()->findByPk($kode); // no idea what this model is responsible for
$es2['t1701'] = '1991-11-19'; // no idea what attribute t1701 is, use descriptive names
$es->update();
}
I have refactored it a bit. Still have no idea what it does ;/
public function actionSimpanEdit($id, $model) {
$editableSaver = new EditableSaver($model);
$editableSaver->update();
$deadline = RealisasiDeadline::model()->findByPk($id);
if($deadline instanceof RealisasiDeadline) {
$deadline->t1701 = '1991-11-19';
if(!$deadline->update()) {
// something went wrong
}
} else {
// not found
}
}
Going back to your problem. It is probably caused by RealisasiDeadline model being not found or some behavior or event preventing it from update.
I need help with tracing this web application. I'm very new to Yii, and I'm trying to dissect an existing app to understand it better. I'm trying to create an edit function, which video tutorials lead me to believe has the exact same process as an add function [save()], except you specify the ID to be overwritten (and I very well could be wrong about this).
Near as I can tell, the following files are in play:
views/forum/view.php
views/forum/_commentform.php
views/forum/_comments.php
controllers/ForumController.php
models/Forum.php
models/Comment.php
I can't really change much of the existing, though I can add my own. It starts with view.php, where much of the stuff is displayed. At the bottom of it is this:
<?php
$this->widget('zii.widgets.CListView',
array('dataProvider'=>$dataProvider, 'itemView'=>'_comments', 'summaryText'=>'',));
?>
_comments.php displays all the usual elements of a comment, like say, from Facebook. There's an edit button there that I made, code here:
<?php echo CHtml::link(CHtml::encode('Edit'),array('forum/editcomment','reply'=>$data->id,'topic'=>$data->content_id)); ?>
That edit button gets the ID of the current comment from the database. Near as the application logs can tell me, this does work.
That calls this particular function in ForumController.php:
public function actionEditComment() {
if(isset($_GET['reply'])) {
$comment=Comment::model()->findByAttributes(array('id'=>$_GET['reply']));
$topic=Forum::model()->findByAttributes(array('id'=>$comment->content_id));
$this->renderPartial('_commentform', array('forum'=>$topic, 'model'=>$comment, 'view'=>'view',));
}
}
Next is the _commentform.php. Nothing much, just a textbox, though it does check if an ID is present; if it is, it is editing an existing comment, otherwise, it is creating a new one. A submit button also changes from Reply to Update, depending on the value of isNewRecord.
EDIT: There's also a CActiveForm, in case that of any help. Might have something to do with routing?
<?php $form=$this->beginWidget('CActiveForm', array(
'id'=>'comment-form',
'action'=>Yii::app()->createUrl('forum/view/id/'.$forum->id),
'enableAjaxValidation'=>false,
)); ?>
<?php
if ($view == 'view') {
echo CHtml::submitButton($model->isNewRecord ? 'Reply' : 'Update', array('id'=>'comment'.$model->id));
}?>
Again, confirmed via application logs, the ID of the comment is being passed through, albeit as id => comment<commentID>. Then this is where things get hazy. I assume the flow goes back to ForumController.php, where, per my logging, the ID is lost.
Here's the parts of the ForumController.php that I deem responsible:
public function actionView() {
$post=$this->loadModel();
$comment=$this->newComment($post);
$viewcount=$post->view_count+1;
$post->view_count=$viewcount;
$post->save();
$this->render('view',array('model'=>$post, 'dataProvider'=>$dataProvider,));
}
private $_model;
public function loadModel() {
if($this->_model===null) {
if(isset($_GET['id'])) {
$this->_model=Forum::model()->findByPk($_GET['id'], $condition);
}
if($this->_model===null)
throw new CHttpException(404,'The requested page does not exist.');
}
return $this->_model;
}
protected function newComment($post) {
$comment=new Comment;
if(isset($_POST['Comment'])) {
$comment->attributes=$_POST['Comment'];
$post->addComment($comment);
}
return $comment;
}
Interestingly, if I write out $comment from newComment() out to the log, it does print out the edited comment (i.e., it prints out "john cena" if I edited the existing comment "who is champ?"), but $comment->id yields a null, which I assume is why instead of updating, the edited comment is saved as a new one.
As for the models, Forum.php and Comment.php strangely point to the same database table, because for some reason they decided to put Forums and Comments into one table. Forum.php also contains the actual addComment function (a placement I find weird), though by the time the flow gets there, the Comment ID is of course null, though the edited comment itself is there.
Where did I go wrong? Did I miss anything?
EDIT: Here's the attributes and rules for the Comment model:
public function attributeLabels() {
return array(
'id' => 'ID',
'node_type' => 'Node Type',
'party_id' => 'Party',
'category' => 'Category',
'title' => 'Title',
'content' => 'Content',
'date_created' => 'Create Time',
'date_modified' => 'Update Time',
'status' => 'Status',);
}
public function rules()
{
/* combine parent and own rules */
$parentRules = parent::rules();
$myRules = array(
array('node_type_id', 'default', 'value'=>'7'), /* set type to Person */
array('node_type_id', 'in', 'range'=>array('7')), /* allow only Person type */
array('party_id, date_created, date_modified, status', 'numerical', 'integerOnly'=>true),
array('category, title, content', 'safe'),
);
/* you want to apply parent rules last, delete them here if necessary */
return array_merge($myRules);
}
Could you post Comment class defenition here?
I think you don't have rule for "id" in Comment::rules(),
When rule for attribute is not defined attribute will be unsafe and you can't set it value by $comment->attributes, or you can change you code to something like:
if(isset($_POST['Comment']) && isset($_POST['Comment']['id'])) {
$comment = Comment::model()->findByPk($_POST['Comment']['id']);
$comment->attributes=$_POST['Comment'];
$post->addComment($comment);
}
My problem is simple, yet I can't figure out how to solve it.
My website is multilanguage. I want the user to be able to add an article in multiple language if he wants, while requiring the inputs of his language (depending on his locale).
Problem is, with CakePHP's conventions about translation, all the inputs must end with the field's name, no matter what language. So all the fields has the same rule for the same field. I can't make one "name" required while another in another language not required.
For example, the default language's input would be:
<input type="text" name="name" required="required" maxlength="45" id="name">
And below that, another language's input for the same field:
<input type="text" name="locales[fr_CA][name]" required="required" maxlength="45" id="locales-fr-ca-name">
The "required" attribute is automatically added to both because of these rules:
$validator
->requirePresence('name', 'create')
->notEmpty('name')
->add('name', [
'length' => [
'rule' => ['minLength', 10],
'message' => 'The title needs to be at least 10 characters long.',
]
]);
Note: I have to change the locale to the default (en_US) when I save to be able to save in multiple languages + the default language (otherwise the default inputs are saved in the default table AND in the i18n table).
if ($this->request->is('post')) {
I18n::locale('en_US');
// ......
EDIT: So here's the complete piece of code when I save (IngredientsController.php)
public function add() {
$ingredient = $this->Ingredients->newEntity();
if ($this->request->is('post')) {
$ingredient = $this->Ingredients->patchEntity($ingredient, $this->request->data);
if(isset($this->request->data['locales'])) {
foreach ($this->request->data['locales'] as $lang => $data) {
$ingredient->translation($lang)->set($data, ['guard' => false]);
}
}
$locale = I18n::locale(); // At this point the locale is fr_CA (not de default)
I18n::locale('en_US'); // Change the locale to the default
if ($this->Ingredients->save($ingredient)) {
$this->Flash->success(__('The ingredient has been saved.'));
I18n::locale($locale); // Put the locale back to the user's locale
return $this->redirect(['action' => 'index']);
} else {
I18n::locale($locale);
$this->Flash->error(__('The ingredient could not be saved. Please, try again.'));
}
}
$this->set(compact('ingredient'));
$this->set('_serialize', ['ingredient']);
}
I set the default locale is the bootstrap.php
/**
* Set the default locale. This controls how dates, number and currency is
* formatted and sets the default language to use for translations.
*/
ini_set('intl.default_locale', 'en_US');
Configure::write('Config.locales', ['fr_CA']);
I determine the user's locale in the AppController.php
public function beforeFilter(Event $event)
{
$locales = Configure::read('Config.locales');
$boom = explode(',', str_replace('-', '_', $_SERVER['HTTP_ACCEPT_LANGUAGE']));
$user_lang = substr($boom[0], 0, 2);
// This piece of code is only to change the locale to fr_CA even if the user's language is just fr or fr_FR
if(in_array($user_lang, Configure::read('Config.langs'))) {
if(in_array($boom[0], $locales)) {
I18n::locale($boom[0]);
} else {
foreach ($locales as $locale) {
if(substr($locale, 0, 2) == $user_lang) {
I18n::locale($locale);
}
}
}
}
$this->set('locales', $locales);
$this->set('locale', I18n::locale());
}
So if I save while being in a different locale than the default, the same default inputs will be saved in the ingredients table AND in the i18n table in fr_CA
Defaults saved in translation table
The fact that the input for the default language is being stored in the translation table in case the default locale has been changed, seems to be the expected behavior, just like when reading data where it will retrieve the data with respect to the current locale, the same applies when saving data.
Cookbook > Database Access & ORM > Behaviours > Translate > Saving in Another Language
Changing the locale to the default is a workaround, but it might be a little too invasive, as it will interfer with any code that uses that value to check the current locale. It's better to directly set the desired locale on the table
$Ingredients->locale(I18n::defaultLocale());
or, which is the least invasive option, on the main entity instead
$ingredient->_locale = I18n::defaultLocale();
Also the former is what the linked docs sesction is describing, but not actually showing, that needs to be fixed.
Fields picking up "wrong" validation rules
While I can see why the form helper, respectively the entity context, picks up validation rules for the "wrong" fields, ie xyz.name fields pick up those for the name field, I can't tell whether this is how it is ment to work.
https://github.com/cakephp/cakephp/blob/3.0.10/src/View/Form/EntityContext.php#L394
https://github.com/cakephp/cakephp/blob/3.0.10/src/View/Form/EntityContext.php#L439
Since it wouldn't pick up nested errors, I guess this is the expected behavior, but I'm not sure, so I'd suggest to create an issue over at GitHub for clarification. In any case, there are various ways to work around this, for example by renaming the fields, or by setting the required option to false.
echo $this->Form->input('locales.fr_CA.name', [
// ...
'required' => false
]);
In your example this is pretty much just a frontend issue, as the fields are not going to be actually validated on the server side.
Another option would be to use a custom translation table class, with validation specific to translations, that actually apply to the used fields, however this is probably not that advisable unless you actually want to apply any validation at all.
Apply validation/application rules to translated columns
For the sake of completion, let's cover validation/application rules too.
In order to actually apply validation and/or application rules, and have them recognized in forms, you'll have use a custom translation table class that holds the rules, and you must use the actual property name that the translate behavior uses for the hasMany associated translation table, which is _i18n.
Here's an example.
src/Model/Table/IngredientsI18nTable.php
namespace App\Model\Table;
use Cake\Datasource\EntityInterface;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
class IngredientsI18nTable extends Table
{
public function initialize(array $config) {
$this->entityClass('Ingredient');
$this->table('i18n');
$this->displayField('id');
$this->primaryKey('id');
}
public function validationDefault(Validator $validator) {
$validator
->allowEmpty('name')
->add('name', 'valid', [
'rule' => function ($value, $context) {
return false;
}
]);
return $validator;
}
public function buildRules(RulesChecker $rules)
{
$rules->add(
function (EntityInterface $entity, $options) {
return false;
},
'i18nName',
[
'errorField' => 'name'
]
);
return $rules;
}
}
IngredientsTable
public function initialize(array $config) {
// ...
$this->addBehavior('Translate', [
// ...
'translationTable' => 'IngredientsI18n'
]);
}
View template
echo $this->Form->hidden('_i18n.0.locale', ['value' => 'fr_FR']);
echo $this->Form->input('_i18n.0.name');
echo $this->Form->hidden('_i18n.1.locale', ['value' => 'da_DK']);
echo $this->Form->input('_i18n.1.name');
// ...
Now the fields will pick up the correct validator, and thus are not being marked as required. Also validation will be applied when creating/patching entities, and finally application rules are being applied too. However I can't guarantee that this doesn't have any side effects, as the Translate behavior internally doesn't seem to account for the situation that the _i18n property has been set externally!
Also you'll still have to set the translations on the entity using translations() in order for the translations to be saved correctly!
foreach ($this->request->data['_i18n'] as $translation) {
$ingredient->translation($translation['locale'])->set('name', $translation['name']);
}
I asked a similar question but I did not provide sufficient details and I got no answers so I will try again.
The main task is to add fields to the CSV file that is exported under the magento admin sales->invoices. I found the main file to edit:
app/code/core/Mage/Adminhtml/Block/Sales/Invoice/Grid.php
This has the options to addColumn's like so:
$this->addColumn('increment_id', array(
'header' => Mage::helper('sales')->__('Invoice #'),
'index' => 'increment_id',
'type' => 'text',
));
Now when I try to add new Column's I change the index to the appropriate database field, lets say 'tax amount' for example. The only problem is that this new value is not in my Magento collection, so it simply populates an empty column in the table.
I am quite new to Magento so I don't fully understand how the Magento collection works or how I can access it for the scope of grid.php. Could someone please give me some direction in how to add to the collection?
I'm really stuck and would appreciate the help.
You basically need to edit the resource model to include the fields you want to include. You can edit the resource in code, I'm not sure what version your using but in the Grid.php file you'll see the _prepareCollection find the code that looks like,
$collection = Mage::getResourceModel('sales/order_invoice_collection')
->addAttributeToSelect('order_id')
->addAttributeToSelect('increment_id')
->addAttributeToSelect('created_at')
->addAttributeToSelect('state')
->addAttributeToSelect('grand_total') ...and so on!
add the line
->addAttributeToSelect('tax_amount')
to that list and the you should be able to use
$this->addColumn('tax_amount', array(
'header' => Mage::helper('sales')->__('Tax'),
'index' => 'tax_amount',
'type' => 'number',
));
This is as untest as I am away from my dev machine and dont have Mage to hand, but this should work or at very least point you in the right direction.
Edit:
Failing that you could try replacing your whole _prepareCollection
protected function _prepareCollection()
{
$collection = Mage::getResourceModel('sales/order_invoice_collection')
->addAttributeToSelect('order_id')
->addAttributeToSelect('increment_id')
->addAttributeToSelect('created_at')
->addAttributeToSelect('state')
->addAttributeToSelect('grand_total')
->addAttributeToSelect('tax_amount')
->addAttributeToSelect('order_currency_code')
->joinAttribute('billing_firstname', 'order_address/firstname', 'billing_address_id', null, 'left')
->joinAttribute('billing_lastname', 'order_address/lastname', 'billing_address_id', null, 'left')
->joinAttribute('order_increment_id', 'order/increment_id', 'order_id', null, 'left')
->joinAttribute('order_created_at', 'order/created_at', 'order_id', null, 'left');
$this->setCollection($collection);
return parent::_prepareCollection();
}
Again this is untested, from memory this is the _prepareCollection from the 1.3 range of magento so its a little old, but quite sure it should work.
I have a controller which I use for a login form. In the view, I have a {error} variable which I want to fill in by using the parser lib, when there is an error. I have a function index() in my controller, controlled by array $init which sets some base variables and the error message to '':
function index()
{
$init = array(
'base_url' => base_url(),
'title' => 'Login',
'error' => ''
);
$this->parser->parse('include/header', $init);
$this->parser->parse('login/index', $init);
$this->parser->parse('include/footer', $init);
}
At the end of my login script, I have the following:
if { // query successful }
else
{
$init['error'] = "fail";
$this->parser->parse('login/index', $init);
}
Now, of course this doesn't work. First of all, it only loads the index view, without header and footer, and it fails at setting the original $init['error'] to (in this case) "fail". I was trying to just call $this->index() with perhaps the array as argument, but I can't seem to figure out how I can pass a new $init['error'] which overrides the original one. Actually, while typing this, it seems to impossible to do what I want to do, as the original value will always override anything new.. since I declare it as nothing ('').
So, is there a way to get my error message in there, or not? And if so, how. If not, how would I go about getting my error message in the right spot? (my view: {error}. I've tried stuff with 'global' to bypass the variable scope but alas, this failed. Thanks a lot in advance.
$init musst be modified before generating your view.
To load your header and footer you can include the following command and the footer's equivalent into your view.
<?php $this->load->view('_header'); ?>
to display errors, you can as well use validation_errors()
if you are using the codeigniter form validation.
if you are using the datamapper orm for codeigniter you can write model validations, and if a query fails due to validation rule violation, you get a proper error message in the ->error property of your model.
Code for your model:
var $validation = array(
'user_name' => array(
'rules' => array('required', 'max_length' => 120),
'label' => 'Name'
)
);
You might try this:
function index() {
$init = array(
'base_url' => base_url(),
'title' => 'Login',
'error' => ''
);
$string = $this->parser->parse('include/header', $init, TRUE);
$string .= $this->parser->parse('login/index', $init, TRUE);
$string .= $this->parser->parse('include/footer', $init, TRUE);
$this->parser->parse_string(string);
}
In parse()you can pass TRUE (boolean) to the third parameter, when you want data returned instead of being sent (immediately) to the output class. By the other hand, the method parse_string works exactly like `parse(), only accepts a string as the first parameter in place of a view file, thus it works in conjunction.