Trigger a new version of Page in SilverStripe - 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();
}
}

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.

Get data in twig function

Is it bad practice to get data from db in twig function or I should pass it to view in controller?
My function is some kind of interface widget that is used on all pages of site admin section. Then on data change I will have to make changes in all actions. But when I get data directly in extension class our teamlead tells that it's bad MVC.
It would be best if you pass it to a view from a controller.
Your team leader is right. What you can do is create an action specific to render that widget. I.e create a custom widget, let's say you want to show the number of current active users:
class WidgetController extends Controller
{
public function usersCountWidgetAction()
{
return $this->render('widget/usersCount.html.twig', array(
"usersCount" => $this->getUsersCount();
));
}
public function getUsersCount()
{
// call the manager and get the result
}
}
Now in all your other twigs you can use
{{ render(controller('AppBundle:Widget:usersCountWidget')) }}

decluttering UI, order Extension gets applied

A few years ago I made a SilverStripe website and added too many fields to Page.php. I'm reworking some of this at the moment but cannot afford do reinvent the Project - now on SilverStripe 3.1.10.
I thought to declutter the UI for Page Sub-Classes, that do not need all the inherited fields, with a few Extensions.
An example how this extension could look
class NoClutter extends Extension {
public function updateCMSFields(FieldList $fields) {
$fields->removeFieldFromTab("Root.Main", "MenuTitle");
$fields->removeFieldFromTab("Root.Main", "Workflow");
}
}
config.yml
RedirectorPage:
extensions:
- NoClutter
This works on all classes for fields added in SiteTree (such as the MenuTitle field), but not for fields added in Page (such as the Workflow field). If the Extension is on UserDefinedForm, Workflow is also removed. But it does not work if the extension is on RedirectorPage. MenuTitle on the other hand is removed in both classes. My guess it's about order. My project is After: 'framework/','cms/' and hope I can make an extension like NoClutter work within the project.
How can I achieve this or how else could I work around the problem?
You need to add $this->extend('updateCMSFields', $fields) at the end of your Page getCMSFields() function.
class Page extends SiteTree {
// ...
public function getCMSFields() {
// call updateCMSFields after adding your fields
SiteTree::disableCMSFieldsExtensions();
$fields = parent::getCMSFields();
SiteTree::enableCMSFieldsExtensions();
// ...
$this->extend('updateCMSFields', $fields);
return $fields;
}
}
$this->extend('updateCMSFields', $fields) declares where your code updateCMSFields() function will get called.
The problem you are having is updateCMSFields() is getting called before you add your custom fields in the Page getCMSFields() function. So you are trying to remove the Workflow field before it is added. This is because the updateCMSFields extension hook is declared in the parent SiteTree getCMSFields() function.
UserDefinedForm solves this by calling $this->extend('updateCMSFields', $fields) at the bottom of its getCMSFields(). SiteTree::disableCMSFieldsExtensions() is required before parent::getCMSFields() is called for the extension hook to work.

theme_links for aliased pages

For the sake of simplicity, let's say I just want to add the 'active' class to all links within the main menu.
The code below works for non-aliased pages. However, when I run a node that has an alias, the active class does not get applied.
I did verify the code was being triggered.
Any thoughts on how to add a class to the main menu links when viewing a node that has a url alias?
function mytheme_links__system_main_menu(array $variables) {
foreach ($variables['links'] as $key => $link) {
$variables[$key]['attributes']['class'][] = 'active';
}
return theme_links($variables);
}
If you using main menu links you would be better off using the context module instead of writing the code yourself. It will save time and headache of testing and allow you and your client to edit the the active links through a user interface instead of hard-coding the classes.
Check it out:
http://drupal.org/project/context

Magento layered navigation on custom product collection

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.
;)

Resources