Silverstripe - show products from parent category and subcategory - silverstripe

I have problem when i try to loop all products from parent category and its sub-catgories.
I create two extra page types:
ProductCategory
Product
In SiteTree (pages) i create structure:
|-Pages
|- Dental equipment (productCategory)
|----- Sub category (productCategory)
|----- Sub sub category (productCategory)
|----- Sub sub sub category (productCategory)
|----- Product 1 (product type)
|----- Product 2 (product type)
|----- Product 3 (product type)
|----- Product 4 (product type)
Now in ProductCategory_Controller i create method to loop all products that not working.
public function Products()
{
$products = Product::get()->filter([
'ParentID' => $this->ID
]);
return $products;
}
My question is how to get all products thats belongs to all parenet and sub-categories?

If you're trying to get the products for the current category page and all parent category pages, then the example code below might point you in the right direction.
If I misunderstood your question, please comment and I'll update my answer.
<?php
namespace {
class ProductCategoryPageController extends PageController
{
public function ProductsInCurrentPageTree()
{
$currentPage = $this->data();
$parentPage = $currentPage->getParent();
if (!$parentPage) {
// No parent page, return the current page products
return ProductPage::get()->filter('ParentID', $currentPage->ID);
}
$pageTreeIds = [
$currentPage->ID,
$parentPage->ID,
];
while ($parentPage = $parentPage->getParent()) {
// Any conditions to break out of the loop, for instance, if your category/product pages does not start from the root level
if (!$parentPage instanceof ProductCategoryPage) {
break;
}
$pageTreeIds[] = $parentPage->ID;
}
return ProductPage::get()->filterAny(['ParentID' => $pageTreeIds]);
}
}
}

Recursively call the same function for child category pages, but when no children, then add products to an array list. Untested code, but should be clear enough:
class ProductCategoryPageController extends PageController {
public function ProductsRecursive($iParentID,$productList) {
$productCategoryPages = ProductCategoryPage::get()->filter(['ParentID' => $iParentID]);
if ($productCategoryPages) {
//assuming product category pages only have children and no products themselves
foreach ($productCategoryPages as $productCategoryPage)
$productList = $this->ProductsRecursive($productCategoryPage->ID,$productList);
}
else {
//assuming you only are collecting product pages that have no children
foreach (ProductPage::get()->filter(['ParentID' => $iParentID]) as $productPage)
$productList->push($productPage);
}
return $productList;
}
public function Products() {
return $this->ProductsRecursive($this->ID, ArrayList::create());
}
}

Related

Elementor Theme Builder + WooCommerce - custom display conditions for single product

Trying to extend the Elementor Pro Theme Builder to include custom display conditions for Single Products. Using the docs here: https://developers.elementor.com/docs/theme-conditions/
Can get the custom condition displaying fine as a general condition, but I can't work out how to get it as a sub condition for single products (therefore it's not currently usable on single product page templates).
Here's the code I'm currently working with - any hints greatly appreciated!
(Function names and logic generalised for client privacy)
function custom_function( $conditions_manager ) {
$conditions_manager->get_condition( 'singular' )->register_sub_condition( new \Custom_Condition() );
}
add_action( 'elementor/theme/register_conditions', 'custom_function' );
class Custom_Condition extends \ElementorPro\Modules\ThemeBuilder\Conditions\Condition_Base {
public static function get_type() {
return 'singular';
}
public function get_name() {
return 'custom_name';
}
public function get_label() {
return esc_html__( 'Custom label', 'custom-label' );
}
public function check( $args ) {
$post_id = $args['id'];
if($logic_check == "1") {
$is_purchasable = 'true';
} else {
$is_purchasable = 'false';
}
return $is_purchasable;
}
}

Iterate through all pages and output in a single html file

I need to output a node (ID 2334) including sub-nodes into a single document to print them later.
How can I load all sub-nodes and output them beneath the main node?
Update
I'm using Drupal 8.
I want to combine all subsites for a given node into a single page.
By sub-nodes / sub-sites I'm referring to the navigation tree.
The key goal is to have a printable version of a whole website section.
Like a category where it combines all related articles into one page.
Update 2
I created a custom page controller and managed to get an array of "sub-nodes":
$subnodes = [];
$tree = \Drupal::menuTree()->load('internal', new \Drupal\Core\Menu\MenuTreeParameters());
foreach ($tree as $item) {
if ($item->link->getRouteParameters()['node'] == $node) {
foreach($item->subtree as $subitem) {
$subnodes[] = $subitem->link->getRouteParameters()['node'];
}
}
}
var_dump($subnodes); // array of ids
I'm now wondering how render the given nodes?
I managed to solve it with a custom controller:
public function exportAll($node) {
$node = (int)$node;
if ($node < 1) {
throw new NotFoundHttpException($node . 'not available');
}
$subnodes = [$node];
$tree = \Drupal::menuTree()->load('internal', new \Drupal\Core\Menu\MenuTreeParameters());
foreach ($tree as $item) {
if ($item->link->getRouteParameters()['node'] == $node) {
foreach($item->subtree as $subitem) {
$subnodes[] = $subitem->link->getRouteParameters()['node'];
}
}
}
$nodes = \Drupal\node\Entity\Node::loadMultiple($subnodes);
return $build = \Drupal::entityTypeManager()->getViewBuilder('node')->viewMultiple($nodes);
}

Sorting list of VirtualPages on a field from its Page

I have an AreaPage with $many_many VirtualPages:
class AreaPage extends Page {
/**
* #var array
*/
private static $many_many = [
'RelatedVirtualPages' => 'VirtualPage'
];
// ...
}
The RelatedVirtualPages are copying content from ContentPages:
class ContentPage extends Page {
/**
* #var array
*/
private static $db = [
'Highlighted' => 'Boolean'
];
// ...
}
What's the best way to sort RelatedVirtualPages on the Highlighted db field of the ContentPage that it's copying?
Virtual Pages could be pointed at pages of different types and there is no enforcement that all of those pages are ContentPages, or at least pages that have a Hightlighted db field. You can ensure this manually when you create your SiteTree, but users could come along and screw it up so keep this in mind.
Here is some psuedo-code that might help you get started. It assumes that all virtual pages are ContentPages. If you will have multiple types of VirtualPages referenced by an AreaPage then this is probably not sufficient.
$virtualPages = $myAreaPage->RelatedVirtualPages();
$contentSourcePages = ContentPage::get()->byIDs($virtualPage->column('CopyContentFromID'));
$sortedSourcePages = $contentSourcePages->sort('Highlighted','ASC');
You possibly could also use an innerJoin, but then you have to deal with _Live tables and possibly multiple page tables (again if not just using ContentPage as VirtualPage) which could lead to some complicated scenarios.
Update
So, to summarize in my own words, you need a list of the VirtualContentPages linked to a specific AreaPage sorted on the Highlighted field from the ContentPage that each VirtualContentPage links to. If this summary is accurate, would this work:
$sortedVirtualPages = $myAreaPage->RelatedVirtualPages()
->innerJoin('ContentPage', '"ContentPage"."ID" = "VirtualContentPage"."CopyContentFromID"')
->sort('Highlighted DESC');
I could not find a very clean method, but did find two ways to achieve this. The function goes in the class AreaPage
First
public function getRelatedVirtualPages()
{
$items = $this->getManyManyComponents('RelatedVirtualPages');
$highlighted = $items->filterByCallback(function($record, $list) {
if($record->CopyContentFrom() instanceOf ContentPage) {
//return ! $record->CopyContentFrom()->Highlighted; // ASC
return $record->CopyContentFrom()->Highlighted; // DESC
}
});
$highlighted->merge($items);
$highlighted->removeDuplicates();
return $highlighted;
}
Second (the method you described in the comments)
public function getRelatedVirtualPages()
{
$items = $this->getManyManyComponents('RelatedVirtualPages');
$arrayList = new ArrayList();
foreach($items as $virtualPage)
{
if($virtualPage->CopyContentFrom() instanceOf ContentPage) {
$virtualPage->Highlighted = $virtualPage->CopyContentFrom()->Highlighted;
$arrayList->push($virtualPage);
}
}
$arrayList = $arrayList->sort('Highlighted DESC');
return $arrayList;
}
I'm not very proud of any of these solutions, but I believe they do fit your criteria.
Here's what I ended up doing, which I think works:
/**
* #return ArrayList
*/
public function VirtualPages()
{
$result = [];
$virtualPages = $this->RelatedVirtualPages();
$contentPages = ContentPage::get()
->byIDs($virtualPages->column('CopyContentFromID'))
->map('ID', 'Highlighted')
->toArray();
foreach($virtualPages as $virtualPage) {
$highlighted = $contentPages[$virtualPage->CopyContentFromID];
$virtualPage->Highlighted = $highlighted;
$result[] = $virtualPage;
}
return ArrayList::create(
$result
);
}
And then it's sortable like so:
$areaPage->VirtualPages()->sort('Highlighted DESC');
Thank you for all the answers and pointers. I'll wait a bit before marking any answer.
Couldn't you just do
//just get one areapage
$AreaPageItem = AreaPage::get()->First();
//now get the RelatedVirtualPages sorted
$related_pages = $AreaPageItem->RelatedVirtualPages()->sort("Highlighted","ASC");

Silverstripe. Modifying gridfieldconfig in modeladmin to add sortable headers

I'm using modeladmin to display a number of event DataObjects.
I've added a number of columns to the summary fields which the client wishes to be able to sort by. Currently, only title is sortable by default. Is it possible to modify gridfieldconfig in modeladmin? In particular to add fields to GridFieldSortableHeader?
Here is my Event dataobject with the summary fields that I need to be able to sort by in modeladmin:
......
static $summary_fields = array('Title', 'DescriptionSummary', 'EventStartDate', 'EventEndDate', 'EventVenue');
static $field_labels = array('DescriptionSummary' => 'Description', 'EventStartDate' => 'Start Date', 'EventEndDate' => 'End Date', 'EventVenue' => 'Venue');
private $widget;
//TO GET THE SUMMARY FIELD VALUES
public function getEventVenue(){
if ($eventVenue = $this->Venue()->Title) return $eventVenue;
return "No Venue specified";
}
public function getEventStartDate(){
if ($startDate = DataObject::get_one('CalendarDateTime', 'EventID = '.$this->ID)) return $startDate->StartDate;
return "No start dates specified";
}
public function getEventEndDate(){
if ($startDate = DataObject::get_one('CalendarDateTime', 'EventID = '.$this->ID)) return $startDate->EndDate;
return "No end dates specified";
}
....
and my event admin:
class EventAdmin extends ModelAdmin {
public static $managed_models = array('CalendarEvent', 'Venue', 'EventCategory');
static $url_segment = 'events';
static $menu_title = 'Events';
}
I've just added some info to doc.silverstripe.org on how to override the edit form and access the GridField within (link). The relevant bits (adapted to your use case):
class EventAdmin extends ModelAdmin {
// ...
public function getEditForm($id = null, $fields = null) {
$form = parent::getEditForm($id, $fields);
$gridField = $form->Fields()->fieldByName($this->sanitiseClassName($this->modelClass));
$gridField->getConfig()->getComponentByType('GridFieldSortableHeader')
->setFieldSorting(array(...));
return $form;
}
}
In case you're trying to sort by the CalendarDate relationship and the EventStartDate field, you'll generally have to override the results list in ModelAdmin, see docs.
While you can add the necessary join there (DataQuery->leftJoin), its not possible
to select additional columns in the query. So that would just allow you to sort by
EventStartDate by default, but not to re-sort the GridField via the UI.
Its a missing feature, we should really support "dot notations" in DataList->sort() out of the box.

How to generate a sitemap ?(asp.net mvc3)

This is my database table
DB Table
how to generate a sitemap?
controller is category
action is Id
that is look like it
Page
TreeMenu.cs:
using System;
using System.Collections.Generic;
namespace Pmvc.Models
{
public partial class TreeMenu
{
public int TreeId { get; set; }
public int TreeParentId { get; set; }
public string TreeName { get; set; }
public List<TreeMenu> Children { get; set; }
}
}
StoreDetailsDynamicNodeProvider.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using MvcSiteMapProvider.Extensibility;
using Pmvc.Models;
namespace Pmvc
{
public class StoreDetailsDynamicNodeProvider : DynamicNodeProviderBase
{
PMVCEntities pmvcDB = new PMVCEntities();
public override IEnumerable<DynamicNode> GetDynamicNodeCollection()
{
var nodes = new List<DynamicNode>();
foreach (var treemenu in pmvcDB.TreeMenu)
{
nodes.Add(CreateNode(treemenu));
}
return nodes;
}
private DynamicNode CreateNode(TreeMenu treemenu)
{
DynamicNode node = new DynamicNode();
node.Action = "ProductDetails";
node.Controller = "Product";
node.Title = treemenu.TreeName;
if (treemenu.Children != null)
{
foreach (var child in treemenu.Children)
{
node.Children.Add(CreateNode(child)); //This line is wrong!
}
}
return node;
}
}
}
wrong:
'MvcSiteMapProvider.Extensibility.DynamicNode' is not include 'Children' definition,and not find Extension Methods 'Children'
public class StoreDetailsDynamicNodeProvider : DynamicNodeProviderBase
{
PMVCEntities pmvcDB = new PMVCEntities();
public override IEnumerable<DynamicNode> GetDynamicNodeCollection()
{
// Build value
var returnValue = new List<DynamicNode>();
// Create a node for each album
foreach (var treemenu in pmvcDB.TreeMenu)
{
DynamicNode node = new DynamicNode();
node.Action = "ProductDetails";
node.Controller = "Product";
node.Title = treemenu.TreeName;
node.RouteValues.Add("id", treemenu.TreeId);
returnValue.Add(node);
}
// Return
return returnValue;
}
}
This is my table
TreeId TreeParentId TreeName
1 0 Category 1
2 1 Menu Item X
3 1 Menu Item Y
4 1 Menu Item Z
5 0 Category 2
6 5 Other Menu 1
7 5 Other Menu 2
8 0 Empty Category
9 7 Menu Lvl 3
How do I write child node code?
Take a look at the MvcSitemap provider. It's pretty easy to write a dynamic provider that reads from your database.
Edit - did you read the documentation or attempt to implement anything? Copied almost verbatim from their site:
Within the sitemap config file, add the dynamic node where appropriate:
<mvcSiteMapNode
title="Details"
action="Details"
dynamicNodeProvider="MyProjectName.MyDynamicNodeProvider, MyProjectName" />
Then, write the actual node provider:
public class MyDynamicNodeProvider
: DynamicNodeProviderBase
{
public override IEnumerable<DynamicNode> GetDynamicNodeCollection()
{
// Build value
var returnValue = new List<DynamicNode>();
// ... Here, get values from your database and build up
// the list of nodes. They can be in a tree structure
// too since it appears that's how your data is - just
// build the nodes in that way with sub-lists.
// Return
return returnValue;
}
}
And that's it.
Edit 2 - addressing other answer.
The code that you provided flattens the tree structure. In your original post you showed that you are already able to print the tree structure. Whatever code you had in your view, you can apply the same principles of a depth-first traversal.
Just in case that was a mockup and not actual code, here's some quick psuedo code to demonstrate an depth-first traversal.
public override IEnumerable<DynamicNode> GetDynamicNodeCollection()
{
// Build value
var nodes = new List<DynamicNode>();
// Get all categories without parents.
var rootCategories = db.GetRootCategories();
// Loop all root level categories, creating a node for each and adding
// to the return value.
foreach (var category in rootCategories)
{
nodes.Add(CreateNode(category));
}
return nodes;
}
private DynamicNode CreateNode(Category category)
{
// Create a new node for the category
DynamicNode node = new DynamicNode();
node.Name = category.Name;
// ... set node properties here ...
// If the category has children, then continue down the tree
// and create nodes for them. This will continue recursively
// to the bottom of the tree.
// Note that I'm just guessing on the property names because
// you didn't show us the code for what your entity actually
// looks like. This is why you should always show what code
// you've attempted - you can get better, more precise answers
// instead of complete guesses.
if (category.Children != null)
{
foreach (var child in category.Children)
{
// For each child, recursively branch into them.
node.Children.Add(CreateNode(child));
}
}
return Node;
}
Please note that this is no way tested or researched - it's only to show a tree traversal.

Resources