We have added in our categories trees some attributes to the categories
Its posible to make a foreach Mage::getModel(custom categories) to get all the value of the custom categories?
You should be able to access those attributes but you will need to load the category first. Now just getting the custom attributes might be a little harder.
$cateogry = Mage::getModel('catalog/category')->load('category here');
//this could get you all attributes
$attributes = $category->getAttributes();
//this would get you just the two attributes that look custom above
$customAttributes = array(
'latitude' => $category->getLatitude(),
'longitude' =>$category->getLongitude(),
);
I hope this helps!
Related
I have a CMS that allows the user to save and create bike tours. Each bike tour also has categories, which are definined using Laravel's Many to Many relationship utilising an intermediary pivot table. At the point of saving a tour, we don't know if the tour is an existing one being edited, or a new one.
I think I should be using Laravel's firstOrNew method for saving the tour, and the sync method for saving categories. However, all the tutorials very simplistically just give the example of passing a single object to the function like so:
$tour = Tour::firstOrNew($attributes);
But what happens when my $attributes also contains extra stuff, like the categories which are linked to a relationship table, and which I will need to save in the next step? For example this very good tutorial gives the following example:
$categories = [7, 12, 52, 77];
$tour = Tour::find(2);
$tour->categories()->sync($categories);
But what happens if the category data is bundled with the data for the rest of the tour, and instead of using find I need to use firstOrNew to create the tour? Should I keep the categories in the $attributes while I instantiate the tour, then run the sync, then unset them before saving the tour, or...? Is there a better way to achieve this?
EDIT: To be clear, the $attributes variable in my example here is essentially the tour object data bundled together- just as the Laravel/Eloquent system would return it from the transaction using the belongsToMany method- with subequent modifications from the user). ie: here is a snapshot of what it contains:
array (
'id' => 1,
'uid' => '03ecc797-f47e-493a-a85d-b5c3eb4b9247',
'active' => 1,
'code' => '2-0',
'title' => 'Tour Title',
'url_title' => 'tour_title',
'distance_from' => 20,
'distance_to' => 45,
'price_from' => '135.00',
'price_to' => '425.00',
'created_at' => '2013-12-31 15:23:19',
'updated_at' => '2015-07-24 16:02:50',
'cats' => // This is not a column name!
array (
0 => 1,
1 => 7
),
)
All of these attributes are column names in my tours table, other than cats, which references another table via a hasMany relationship. Do I need to unset it manually before I can set this object class and save it with $tour->save?
I am looking for the cleanest most Laravel way to do it?
EDIT2: Here is the relationship defined in the Tours model:
class Tour extends Model
{
protected $guarded = [];
public function cats(){
return $this->belongsToMany('App\TourCategory', 'tour_cat_assignments', 'tour_id', 'cat_id');
}
}
you need to define $fillable property of your Tour model to tell eloquent which attributes to consider when using mass assignment so it will ignore categories related attributes silently. for ex.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Tour extends Model {
protected $fillable = ['name'] //... other attributes which are part of this model only and laravel will consider only these attributes and ignore category related attributes which you can consider later use.
}
You can use firstOrCreate. The data actually gets persisted using this method.
$categories = [7, 12, 52, 77];
$tour = Tour::firstOrCreate($attributes)->cats()->sync($categories);
Got to make sure the fields are mass-assignable to be able to use the firstOrCreate method though. So either set the fieldnames in the $fillable property or put this in the Tour model:
protected $guarded = [];
Since you have mentioned "CMS" and "subsequent modifications from user", I guess that you are getting your attributes from a Form which means you are getting a Request object/collection.
If that is the case then you can try
$tour = Tour::firstOrCreate($request->except('cats'));
$categories = [];
foreach($request->get('cats') as $key=>$value){
$categories[] = $value;
}
$tour->cats()->sync($categories);
However, if your $attributes us constructed as an array (probably with some manipulations on form data) as per your EDIT then in that case you may try:
$tour = Tour::firstOrCreate(array_except($attributes, ['cats']);
$categories = [];
foreach($attributes['cats'] as $key=>$value){
$categories[] = $value;
}
$tour->cats()->sync($categories);
In any case, you must have the mass assignable fields declared in $fillable property in your model i.e. Tour.
Hope this helps.
I want to programmatically create a product attribute dropdown that shows all categories that the product is assigned to. With this, I would like to define a default category that can be used in the products 'canonical' tag url.
In order to do that I started creating a source model, and in this source model I want to access the product to dynamically create the dropdown options.
Is this possible? If so, how should I go about it? Or am I looking in the wrong place?
I found the answer thanks to a colleague of mine:
To create a product attribute which lists all categories of a product, I can load the product from the Magento registry via Mage::registry('current_product). This leads to the following content in the getAllOptions() method:
public function getAllOptions()
{
$categoryIds = Mage::registry('current_product')->getCategoryIds();
$this->result[] = array(
'value' => '-1',
'label' => 'None'
);
foreach ($categoryIds as $categoryId) {
$this->result[] = array(
'label' => Mage::getModel('catalog/category')->load($categoryId)->getName(),
'value' => $categoryId
);
}
return $this->result;
}
I am trying to edit the Orders Grid so that I can export data to XML with the quantity of each item sold on each line.
Now I can only access the total amount of the order, which is not sufficient. I would like to build a Grid with the information available for each order in Order View > Information > Ordered items.
Is this possible with a few lines of code ?
Here is what I did for now :
I tried to manually add columns in the _prepareColumns() function from Grid.php.
Basically, I tried to add a quantity column :
$this->addColumn('total_qty_ordered', array(
'header' => Mage::helper('sales')->__('Qty'),
'index' => 'total_qty_ordered',
'filter_index' => 'sales_flat_order.total_qty_ordered',
));
However I do not get any total quantity, and of course I do not get the product split in each order. I do not really know where to look to implement this product split.
Thanks by advance.
EDIT :
Here is what is I get thanks to the extension
Order grid
However, I cannot export this product split because the last column is a kind of 'embedded' of other information. So I get an empty column in XML.
I don't usually recommend extensions -- I believe most of them can be a bit more trouble than they are worth, but in this case, I do recommend this one:
http://www.magentocommerce.com/magento-connect/enhanced-admin-grids-editor.html
It's free -- code is fairly clean and you can add that by clicking the grid customization button on the orders page, clicking more options, then finding the items drop down and adding those columns in. Will show you a table of all of the items.
If you want to post the code you've written the custom way, I can help you customize that to do the job too.
We published a whole blog post on how to add any data to your order grid. Hope this will help you! https://grafzahl-io.blogspot.de/2016/11/how-to-display-m2e-order-data-or.html
So the solution would be to copy the Sales Grid Block to a local module and add your column like this example:
$this->addColumn('order_type', array(
'header' => Mage::helper('sales')->__('Order Type'),
'width' => '100px',
'align' => 'left',
'index' => 'order_type',
'renderer' => 'yourmodule/adminhtml_sales_grid_renderer_m2eAttribute',
'filter_condition_callback' => array($this, '_filterM2eConditionCallback')
));
The filter_condition_callback is a method in the Grid Block. The renderer is another class as you can see in the namespace. In the renderer you can define what is displayed in your column. The filter_condition_callback defines how the grid should act, in case someone will filter by your custom column.
It will look like this:
/**
* filter callback to find the order_type
* of orders through m2e (amazon, ebay, ...)
*
* #param object $collection
* #param object $column
* #return Yourname_Yourmodule_Block_Adminhtml_Sales_Order_Grid
*/
public function _filterM2eConditionCallback($collection, $column) {
if (!$value = $column->getFilter()->getValue()) {
return $this;
}
if (!empty($value) && strtolower($value) != 'magento') {
$this->getCollection()->getSelect()
// join to the m2mepro order table and select component_mode
->join(
'm2epro_order',
'main_table.entity_id=m2epro_order.magento_order_id',
array('component_mode')
)
->where(
'm2epro_order.component_mode = "' . strtolower($value) . '"');
} elseif(strtolower($value) == 'magento') {
$this->getCollection()->getSelect()
->join(
'm2epro_order',
'main_table.entity_id=m2epro_order.magento_order_id',
array('component_mode')
)
->where(
'm2epro_order.component_mode = NULL');
}
return $this;
}
As you can see, there are two joins to collect the data we need to filter for.
This is how the renderer looks like which will display the data in the grid:
class Yourname_Yourmodule_Block_Adminhtml_Sales_Grid_Renderer_M2eAttribute
extends Mage_Adminhtml_Block_Widget_Grid_Column_Renderer_Abstract
{
public function render(Varien_Object $row)
{
// do whatever you need, to display your data
// get the id of the row order data
$orderId = $row->getEntityId();
// get the related m2e order data
$orders = Mage::getModel('M2ePro/Order')
->getCollection()
->addFieldToSelect('component_mode')
->addFieldToFilter('magento_order_id', $orderId);
if($orders) {
$data = $orders->getFirstItem()->getData();
if(isset($data['component_mode'])) {
return ucfirst($data['component_mode']);
}
}
// return the string "magento" if there is no m2e relation
return 'Magento';
}
}
The link will show you other example, how to show data in the order grid.
I am looking to add a column to the product grid (in the admin area to be clear) to display how many times this product has been sold. Here is what I have so far after piecing together from several other posts:
In app/code/local/Namespace/Qtysold/Block/Adminhtml/Catalog/Product/Grid.php
<?php
class Namespace_Qtysold_Block_Adminhtml_Catalog_Product_Grid extends Mage_Adminhtml_Block_Catalog_Product_Grid
{
/* Overwritten to be able to add custom columns to the product grid. Normally
* one would overwrite the function _prepareCollection, but it won't work because
* you have to call parent::_prepareCollection() first to get the collection.
*
* But since parent::_prepareCollection() also finishes the collection, the
* joins and attributes to select added in the overwritten _prepareCollection()
* are 'forgotten'.
*
* By overwriting setCollection (which is called in parent::_prepareCollection()),
* we are able to add the join and/or attribute select in a proper way.
*
*/
public function setCollection($collection)
{
/* #var $collection Mage_Catalog_Model_Resource_Product_Collection */
$store = $this->_getStore();
if ($store->getId() && !isset($this->_joinAttributes['qty_sold'])) {
$collection->joinAttribute(
'qty_sold',
'reports/product_collection',
'entity_id',
null,
'left',
$store->getId()
);
}
else {
$collection->addAttributeToSelect('qty_sold');
}
echo "<pre>";
var_dump((string) $collection->getSelect());
echo "</pre>";
parent::setCollection($collection);
}
protected function _prepareColumns()
{
$store = $this->_getStore();
$this->addColumnAfter('qty_sold',
array(
'header'=> Mage::helper('catalog')->__('Qty Sold'),
'type' => 'number',
'index' => 'qty_sold',
),
'price'
);
return parent::_prepareColumns();
}
}
A couple of things here. 1) $store->getId() returns 0 so it never goes into that first block in setCollection, is that correct behavior since it is the Admin area? 2) If I force the joinAttribute to run, it causes an exception (Invalid entity...) which is sort of expected since reports doesn't appear to really be an entity, but I'm not really clear on this whole entity business. 3) In other examples (like this one: http://www.creativemediagroup.net/creative-media-web-services/magento-blog/30-show-quantity-sold-on-product-page-magento) they use something like this:
$_productCollection = Mage::getResourceModel('reports/product_collection')
->addOrderedQty($from, $to, true)
->addAttributeToFilter('sku', $sku)
->setOrder('ordered_qty', 'desc')
->getFirstItem();
And I am not sure if there is any way to "join" with this reports/product_collection or if there is any way to recreate its "addOrderedQty" data?
This is on Magento 1.7. I can provide further details as needed. I am a beginner with Magento development so any help at all (including resources to learn) would be greatly appreciated. Thanks!
1) Admin Area does have the store ID = 0, so yes this will always be returned.
this will also mean your conditional always fails and never does any joining, it will just try and add the qty_sold to the collection, which of course will not work as it's not part of that entities data.
The issue is the joinAttribute method will only work on "Entites" (it depends on the classes used by the models you are trying to join), and as the reports/product collection isn't one of these you will have to join another way using methods like this:
join() or joinLeft()
with this kind of thing:
$collection->getSelect()
->join(
'customer_entity',
'main_table.customer_id = customer_entity.entity_id',
array('customer_name' => 'email')
);
Was also dealing with this issue and solved it as follows:
protected function _prepareCollection() {
// [...]
$collection = Mage::getModel('catalog/product')->getCollection();
// Add subquery join to sales_flat_order_item to get a SUM of how many times this product has been ordered
$totalOrderedQuery = Mage::getSingleton('core/resource')->getConnection('core_read')
->select()
->from('sales_flat_order_item', array('product_id', 'qty_ordered' => 'SUM(`qty_ordered`)'))
->group('product_id');
$collection->joinField('qty_ordered', $totalOrderedQuery, 'qty_ordered', 'product_id=entity_id', null, 'left');
// [...]
return parent::_prepareCollection();
}
Note the usage of $collection->joinField(), tried using the mentioned $collection->getSelect()->join(), but it gives all sorts of issues with sort orders and filtering due to the internal Magento data collection not recognizing the column as part of the data set.
Then in _prepareColumns you can simply add the column:
$this->addColumn('qty_ordered', array(
'header' => Mage::helper('xyz')->__('Total quantity ordered'),
'sortable' => true,
'width' => '10%',
'index' => 'qty_ordered',
));
You might want to add a renderer which checks and converts a NULL value to '0', otherwise you will end up with empty columns if the product hasn't been ordered yet (you could also fix this in the SELECT query by using IFNULL for qty_ordered).
I was able to achieve what I needed (though it is hardly perfect) thanks to that tip from Andrew. Here is my updated code for setCollection:
public function setCollection($collection)
{
/* #var $collection Mage_Catalog_Model_Resource_Product_Collection */
$store = $this->_getStore();
$collection->getSelect()->joinLeft(
array('oi' => 'sales_flat_order_item'),
'e.entity_id = oi.product_id',
array("qty_sold" => 'SUM(oi.qty_ordered)')
)->group('e.entity_id');
parent::setCollection($collection);
}
I was curious if any Magento developers see a serious flaw in this approach (I'm aware of some of the business logic such as not counting products that were canceled, etc) or recommendations for a better approach. Either way this is "good enough" for now and meets my needs, so I'll post in case others need it.
I had a problem sorting a paginated list when using a calculated field such as COUNT() in cakephp 1.3
Let's say that i have two models: Article and Comments (1 article x N comments) and i want to display a paginated list of the articles including the number of comments for each one. I'd have something like this:
Controller:
$this->paginate = array('limit'=>80,
'recursive'=>-1,
'fields'=>array("Article.*","COUNT(Comment.id) as nbr_comments"),
'joins'=>array(array( 'table' => 'comments',
'alias' => 'Comment',
'type' => 'LEFT',
'conditions' => array('Comment.article_id = Article.id'))
),
'group'=>"Article.id"
);
(i had to overwrite the findCount() method in order to paginate using group by)
The problem is that in the view, the sort() method won't work:
<th><?php echo $this->Paginator->sort('nbr_comments');?></th> //life is not that easy
I was able to create a workaround by "cheating" the pagination and sort:
Controller
$order = "Article.title";
$direction = "asc";
if(isset($this->passedArgs['sort']) && $this->passedArgs['sort']=="nbr_comments")
$order = $this->passedArgs['sort'];
$direction = $this->passedArgs['direction'];
unset($this->passedArgs['sort']);
unset($this->passedArgs['direction']);
}
$this->paginate = array(... 'order'=>$order." ".$direction, ...);
$this->set('articles', $this->paginate());
if($order == "clicks"){
$this->passedArgs['sort'] = $order;
$this->passedArgs['direction'] = $direction;
}
View
<?php $direction = (isset($this->passedArgs['direction']) && isset($this->passedArgs['sort']) && $this->passedArgs['sort'] == "nbr_comments" && $this->passedArgs['direction'] == "desc")?"asc":"desc";?>
<th><?php echo $this->Paginator->sort('Hits','clicks',array('direction'=>$direction));?></th>
And it works.. but it seems that is too much code for something that should be transparent to the developper. (It feels like i'm doing cake's work) So i'm asking if there's another simpler way. Maybe cake has this functionallity but decided to hide it.. o_O.. there's nothing about this on the documentation, and i haven't found another good solution on S.O... how do you do it?
Thanks in advance!
maybe your problem can be solved using Virtual fields ?
If you create a custom field for your computed field, you will be able to sort using Paginator->sort() method on that custom field.
I found that solution there in the comments (there is no need to customize the paginate method etc. using custom fields instead of in adnan's initial solution).
What you may want to do is use Cake's counterCache functionality in your model definition. Rather than calculating the comment counts at read time, you add nbr_comments as an int field in your articles table. Every time a Comment is inserted or deleted, nbr_comments will be updated automatically.
In your Comment model:
var $belongsTo = array(
'Article' => array(
'counterCache' => 'nbr_comments'
)
);
Now you can just use $this->Paginator->sort('Article.nbr_comments'); You wont need to do anything funky in your controller.
My solution for this problem was the following:
put your calculate field as a virtual field
$this->Comment->virtualFields = array(
'nbr_comments' => 'COUNT(Comment.id)'
);
then, you can use that field, in your paginate order variable
$order = array("Comment.nbr_comments" => "DESC");
$this->Paginator->settings = array("order" => $order);