Magento layered navigation on custom product collection - collections

I have been working on a custom module for Magento (ver. 1.8.0.0) that shows a list of related products of a certain product.
In order to achieve this I have created my own module by overwriting the Mage_Catalog_Block_Product_List class.
Basically here's how it works:
From a controller I catch the products entity_id and I store the product in the registry so I can use it inside my custom written Block which is called list.php
Here is the method that fills the product collection:
protected function _getProductCollection()
{
if (is_null($this->_productCollection)) {
$prod = Mage::registry('chosenproduct');
$this->_productCollection = $prod->getRelatedProductCollection()
->addAttributeToSelect('required_options')
->addAttributeToFilter(array(array('attribute'=>'accessory_manufacturer','neq'=>false)))
->addAttributeToSort('position', 'asc')
->addStoreFilter()
->setPageSize(30)
->setCurPage(1);
;
$this->_addProductAttributesAndPrices($this->_productCollection);
Mage::getSingleton('catalog/product_visibility')->addVisibleInCatalogFilterToCollection($this->_productCollection);
$this->setProductCollection($this->_productCollection);
}
return $this->_productCollection;
}
I also added the following in the layout .xml of my custom module to make sure the layered navigation shows:
<reference name="left">
<block type="catalog/layer_view" name="catalog.leftnav" after="currency" template="catalog/layer/view.phtml"/>
</reference>
The layered navigation shows, but it seems that it is taking all products as collection instead of the custom collection that is used in the method I added above.
I also know that I can get the catalog/layer using this $layer = Mage::getSingleton('catalog/layer');
The layer class also has a method called prepareProductCollection and setCollection but for some reason I can't get it to work.
Any help on this?
Basically I want to have the layered navigation for the products that are in the custom collection.
Thanks,

I just managed to achieve what I wanted. I have overwritten both the Mage_Catalog_Model_Layer class and the Mage_Catalog_Model_Category
Both now have a new variable called $_customCollection: protected $_customProductCollection;
I have overwritten the getProductCollection() in both classes and I added this in the beginning of the method:
if(isset($this->_customProductCollection)){
return $this->_customProductCollection;
}
I have also a method that allows me to set this "customProductCollection" inside both these classes. Once It's set, the rest of the data of the layered navigation/category is based on this collection.
;)

Related

Silverstripe 4 - Adding a FormAction via getCMSFields

Goal:
I have a DataObject called "Event". This is in a managed_model for "EventsAdmin" (extending ModelAdmin). When editing an Event, I want a tab on the record called "Moderation" that has a few fields and two buttons: "Approve" and "Reject". These two buttons call an action each that performs relevant actions.
Event extends DataObject
public function getCMSFields() {
$fields = parent::getCMSFields();
$eventStatus = $fields->dataFieldByName("EventStatus")
->setTitle('Current Status')
->setDisabled(true);
$approveButton = FormAction::create('doApproveEvent', _t('SiteBlockAdmin.Approve', 'Approve'))
->setUseButtonTag(true)
->addExtraClass('btn-outline-success font-icon-check-mark-circle');
$rejectButton = FormAction::create('doRejectEvent', _t('SiteBlockAdmin.Reject', 'Reject'))
->setUseButtonTag(true)
->addExtraClass('btn-outline-danger font-icon-cancel-circled');
$fields->addFieldsToTab('Root.Moderation', [
$eventStatus,
$approveButton,
$rejectButton
]);
return $fields;
}
This displays the buttons just fine. But they don't do anything. So I am trying to work out how they can plug into action methods doApproveEvent and doRejectEvent (And where they should go)
I did find docs that led me to adding the buttons to the action bar at the bottom of the CMS page via updateFormActions(). But this isn't what I want as the other fields I am adding above the buttons are part of the Approve/Reject process. Here is the code for this method. This works fine barring the buttons are not in a logical place for the process I'm trying to create.
class CMSActionButtonExtension extends DataExtension
{
public function updateFormActions(FieldList $actions)
{
$record = $this->owner->getRecord();
if (!$record instanceof Event || !$record->exists()) {
return;
}
$approveButton = FormAction::create('doApproveEvent', _t('SiteBlockAdmin.Approve', 'Approve'))
->setUseButtonTag(true)
->addExtraClass('btn-outline-success font-icon-check-mark-circle');
$rejectButton = FormAction::create('doRejectEvent', _t('SiteBlockAdmin.Reject', 'Reject'))
->setUseButtonTag(true)
->addExtraClass('btn-outline-danger font-icon-cancel-circled');
$actions->push($approveButton);
$actions->push($rejectButton);
}
public function doApproveEvent($data, $form) {
$record = $this->owner->getRecord();
// Approve logic
}
public function doRejectEvent($data, $form) {
$record = $this->owner->getRecord();
// Reject logic
}
}
The above Extension is attached to GridFieldDetailForm_ItemRequest
extension.yml
SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest:
extensions:
- My\Namespace\CMSActionButtonExtension
Interestingly, if I have both sets of buttons on the page at the same time, the updateFormActions option works while my desired option still doesn't. Despite the buttons being of identical markup and sitting inside the exact same form tag. I assume that has something to do with how Silverstripe loads the main content panel and the DOM.
Any thoughts on achieving this? Anyone seen a button added to the main CMS panel in a module that I could take a look at? I found this post from 5 years ago, but it's for SS3 and the answer doesn't work for me.
Short answer:
you have to add custom FormActions through an Extension on the Controller that controls the form (or on the form itself
Long Answer:
A bit of background on how SilverStripe does forms:
Generally speaking, forms are always served through Controllers/RequestHandlers (they need to be accessible on some route, usually that's an Action on a Controller that is often named Form, EditForm, ItemEditoForm, ...).
Fields
Inside the CMS you rarely ever have to create your own form, that's done by the CMSs built in Controllers/RequestHandlers for the admin area (GridFieldDetailForm_ItemRequest in this case).
Basically (pseudo code here), what those controllers do is:
public function EditForm() {
$fields = $myCurrentlyEditingDataObject->getCMSFields();
$actions = ...;
$validator = ...;
$this->updateFormActions(&$actions);
$form = new Form('ItemRequestForm', $fields, $actions, $validator);
$this->updateItemEditForm(&$form); // or $this->updateEditForm()
return $form;
}
So, getCMSFields() and in some cases getCMSActions()/getCMSValidator() (not sure if those 2 are still used in SilverStripe 4.x), you can add things to the form, without ever seeing the form object.
Also, the getCMSFields() will always be put into the ``` section of the Form, that's why your button is somewhere in the middle with all the fields and not with the other actions.
Submission
When a form is submitted (eg to /admin/pages/edit/EditForm/265/field/NameOfMyGridField/item/542/ItemEditForm), it will call the action GridFieldDetailForm_ItemRequest->ItemEditForm() which returns the Form object where subsequently FormRequestHandler->httpSubmission() is called. This will then look at the submitted data to figure out what action was clicked (eg $_REQUEST['action_doApproveEvent']) and try to find that action.
The way it tries to find that, is checking if it itself has a method called doApproveEvent, if that fails, it will try Form->getController()->doApproveEvent() or something like that. In the case of a GridField, that controller is GridFieldDetailForm_ItemRequest which means it will try to call GridFieldDetailForm_ItemRequest->doApproveEvent()
So, that means DataObject->getCMSFields() lets you easily add FormFields (and FormActions) into your form body.
But it does not provide a means of adding a method to handle the submission.
That's why, for custom actions you need to modify the Controller (GridFieldDetailForm_ItemRequest in this case).
You are doing this by creating a Extension which you attached to GridFieldDetailForm_ItemRequest.
Any method in your Extension is added to the thing it's attached to, so if you add a method called updateFormActions, it will kind of become GridFieldDetailForm_ItemRequest->updateFormActions().
And if you recall from earlier, the controller will call $this->updateFormActions() during the creation of the form.
Additionally, as I explained earlier, when a FormAction is named doApproveEvent it will look for a GridFieldDetailForm_ItemRequest->doApproveEvent(), which now exists because you added it through that Extension.
So, in summary: you have to add custom FormActions through an Extension on the Controller that controls the form (or on the form itself
PS: the old post from
bummzack you linked to worked in 3.x, because the Controller in his example that created the form was an instance of LeftAndMain.

Extend templates with dataextensions/modules content - SilverStripe 3.4

I could add extra functionality to a dataobject if I extend it with a dataextension. For example I've got an Item which gets extended from a module with stock keeping functionality. Let's say the Item also gets's extended form a few other modules.
After the extension with the stock keeping functionality, I'd like to display the availability of the item in the frontend, for example with a green/red dot. How can I get this dot's markup inside my template for the detail page(ItemPage.ss) and the include (Item.ss) for the overview page of items without overwriting the whole template. Just adding this one part, like the way I extend a function on my base class?
That could be a way to add extra markup to the original template.
Inside dataobject or global dataobject extension
public function ExtraTemplateHTML($position) {
$html = null;
foreach($this->owner->extend('updateExtraTemplateHTML') as $positionBlocks) {
if(isset($positionBlocks[$position])) {
foreach($positionBlocks[$position] as $htmlBlock) {
$html .= $htmlBlock->getValue();
}
}
}
return $html;
}
Inside the specific dataobject extension
public function updateExtraTemplateHTML($htmlBlocks) {
$viewer = new SSViewer(__CLASS__);
$html = $viewer->process($this->owner);
$htmlBlocks['bottom'][] = $html;
$topHtml = HTMLText::create();
$topHtml->setValue(123);
$htmlBlocks['top'][] = $topHtml;
return $htmlBlocks;
}
Original Template
$ExtraTemplateHTML(top)
...
...
$ExtraTemplateHTML(bottom)
Extension Template
Than just write a new template for your extension with the content you would like to add.
You cannot partially change a template, you can only substitute the template with another. So, keeping this in mind you should keep modular architecture with many logical includes.
Another possible way to extend existing content is using DOM and javascript. But you should think about side effects. For example if you add extra textual content then it won't be visible by crawlers and will effect your SEO. But for decorative enhancements, like adding extra coloured dot, this approach will work.

Trigger a new version of Page in SilverStripe

I am wondering if there is a way to create a new version on a Page object in SilverStripe through code.
I am statically caching pages using the staticpublisher module. The issue I am facing is that when a DataObject is saved, it doesn't trigger a publish on the parent page, so the cache version is out of date. I have overcome this by running a doPublish() on the parent Page object. But that will obviously publish the page even if the publisher isn't ready for the new changes to go live.
Here is what I have currently on my DataObject:
function onAfterWrite() {
parent::onAfterWrite();
// Get the current page
$page = Controller::curr()->currentPage();
if($page) {
// Publish the page
$page->doPublish();
}
}
Is there a way to create a new version of the page upon saving the DataObject and setting the Page to draft?
I have interrogated this Versioned class but could not get anything to work from there.
Any ideas would help.
I'm assuming you've created a relationship between your SiteTree object and your DataObject (i.e. a hasOne, hasMany, or ManyMany). If that is the case, you should have a reverse relationship from the DO back to SiteTree (let's call it ParentPage).
You can trigger a draft save on your page using the DataObject's onAfterWrite() call.
class MyDataObject {
//define a relationship back to the parent
private static $belongs_to = array('ParentPage' => 'Page');
//define this function on your DataObject
public function onAfterWrite() {
parent::onAfterWrite();
//trigger a write (but not a publish) on your parent page
$this->ParentPage()->write();
}
}

How to programatically disable regions on a drupal 7 page?

I am working on a module where i have a page that must have no regions or extra content. A kind of "please wait" page.
How do i diable all extra content (regions menus...etc) ? i think Panels has this ability but i can't find the snippet it uses.
On another hand is it possible for a module to specify a special custom page ? like the maintenance-page for example ?
The page.tpl.php method is not flexible. It is based on a presentation logic. You should use hook_page_alter() for a business logic solution. For example:
function yourmodulename_page_alter(&$page) {
if (current_path() == 'node/add/yourcontenttype') {
unset($page['sidebar_first']);
}
}
Also look at very powefull Context module.
You can create a an extra page.tpl.php specifically for the page where you want to hide the regions. The naming principle is similar to the one for nodes.
Let's say you have a page with the url example.com/content/contact. A template named page--content--contact.tpl.php would serve that page and any page that starts with that url, i.e. the page example.com/content/contact/staff would also use that template (I think).
Check the classes of the body element for clues to what you can name your template, most themes will print that. In my example above, the body element would include the class page-content-contact.
Only thing i can think of is writing checks in your page.tpl.php file to see if you on that "page" your talking about and not printing out the regions/menus, or use a different template. http://drupal.org/node/223440
If you want to do this before the blocks are rendered:
/**
* Implements hook_block_list_alter()
*
* Hides the right sidebar on some pages.
*/
function THEME_NAME_block_list_alter(&$blocks) {
// This condition could be more interesting.
if (current_path() !== 'node/add/yourcontenttype') {
return;
}
// Go through all blocks, and hide those in the 'sidebar_second' region.
foreach ($blocks as $i => $block) {
if ('sidebar_second' === $block->region) {
// Hide this block.
unset($blocks[$i]);
}
}
}
Note: Interestingly, this hook seems to work no matter if you have it in your theme or in a module.
(Please correct me if I'm wrong)

Drupal Views exposed filter of Author name as a drop down

This is a follow up question to Drupal Views exposed filter of Author name. The following question was answered and works. I can filter a view by user name. The user name is entered is entered by typing in a box and the box then auto completes. Rather then doing this I would like the list of users as a drop down. I only need one user to be selected. Do you know if this is possible?
You'll need a custom module for that.
I've done this for Drupal 7 this way: create a module, say, views_more_filters, so you have a views_more_filters.info file like this:
name = Views More Filters
description = Additional filters for Views.
core = 7.x
files[] = views_more_filters_handler_filter_author_select.inc
files[] = views_more_filters.views.inc
(file views_more_filters_handler_filter_author_select.inc will contain our filter handler).
A basic views_more_filters.module file:
<?php
/**
* Implements of hook_views_api().
*/
function views_more_filters_views_api() {
return array('api' => 3);
}
Then define your filter in views_more_filters.views.inc:
<?php
/**
* Implements of hook_views_data().
*/
function views_more_filters_views_data() {
return array(
'node' => array(
'author_select' => array(
'group' => t('Content'),
'title' => t('Author UID (select list)'),
'help' => t('Filter by author, choosing from dropdown list.'),
'filter' => array('handler' => 'views_more_filters_handler_filter_author_select'),
'real field' => 'uid',
)
)
);
}
Note that we set author_select as a machine name of the filter, defined filter handler ('handler' => 'views_more_filters_handler_filter_author_select') and a field we will filter by ('real field' => 'uid').
Now we need to implement our filter handler. As our filter functions just like default views_handler_filter_in_operator, we simply extend its class in views_more_filters_handler_filter_author_select.inc file:
<?php
/**
* My custom filter handler
*/
class views_more_filters_handler_filter_author_select extends views_handler_filter_in_operator {
/**
* Override parent get_value_options() function.
*
* #return
* Return the stored values in $this->value_options if someone expects it.
*/
function get_value_options() {
$users_list = entity_load('user');
foreach ($users_list as $user) {
$users[$user->uid] = $user->name;
}
// We don't need Guest user here, so remove it.
unset($users[0]);
// Sort by username.
natsort($users);
$this->value_options = $users;
return $users;
}
}
We haven't had to do much here: just populate options array with a list of our users, the rest is handled by parent class.
For further info see:
Views API
Where can I learn about how to create a custom exposed filter for Views 3 and D7? on Drupal Answers
Demystifying Views API - A developer's guide to integrating with Views
Sophisticated Views filters, part2 - writing custom filter handler (in Russian, link to Google translator)
Tutorial: Creating Custom Filters in Views
Yes, this is possible. Its not particularly tough to do this... but its slightly tedious. You need to create two views
The first view is a list of users on your system (a View of type Users). This user list is displayed as a dropdown instead of a list (using jump menu view style). Clicking on any user within this dropdown will call the second view with the uid (user id) of the selected user as the argument in the URL. This view is a block.
The second view is a simple Node listing. It is a page view at a particular URL. It takes 1 argument which is the uid (user id) of the user.
Detailed Steps
Download the Ctools module
http://drupal.org/project/ctools
Enable the Chaos Tools Module. This
module provides a Views Style Plugin
called "Jump Menu"
Create a new view of type Users and NOT type Node which you usually
create. In the fields add User:
Name and User: uid. For the
settings of User: uid, make sure
you click on Rewrite the output of
the field. The rewritten output of
the field should be
my_node_list/[uid]. Make sure you
select the exclude from display checkbox.
In the settings for Style in the view, select the Jump Menu style. Click on the settings for the style. Make sure the Path dropdown has User: uid choosen
Add a block display to the view. Name the block User Drop Down
Save the view
Add the block User Drop Down to any region in your theme e.g. Content Top (usually the best) or left sidebar. Make sure the block is only visible at the urls my_node_list/* and my_node_list by setting the block visibility settings
Now create another view of type Node. Add an argument field User: uid. Add the fields you are interested in e.g. Node: title, User: Name etc.
Add a page display. Let the page be at the url my_node_list
Save the view. Test the dropdown with its list of users on the system at http://yoursitename/my_node_list
http://drupal.org/project/better_exposed_filters
Check this one
I think you just have to choose "Taxonomy:term The taxonomy term ID" instead of "name".
Found a simple solution here. http://bryanbraun.com/2013/08/06/drupal-tutorials-exposed-filters-with-views

Resources