SilverStripe - Custom Faceted Search Navigation - php

I'm working on page for a SilverStripe that will allow users to sort through portfolio pieces based on the selected facets.
Here are the key points/requirements:
I have 2 facet categories they can search by: Media Type (i.e. Ads,
Posters, TV, Web) and Industry (Entertainment, Finance, Healthcare,
Sport, etc).
Users should be allowed to search multiple facets at once and also
across media type and industry at once.
In the SilverStripe admin, since content managers need to be able
to maintain the facet namesfor Media Type and Industry, I made it so
that there are 2 admin models where the names can be entered:
MediaTypeTagAdmin and IndustryTagAdmin. Here are the data object
classes for MediaTypeTag and IndustryTag that are used by the admin
models:
MediaTypeTag class
<?php
class MediaTypeTag extends DataObject {
private static $db = array(
'Name' => 'varchar(250)',
);
private static $summary_fields = array(
'Name' => 'Title',
);
private static $field_labels = array(
'Name'
);
private static $belongs_many_many = array(
'PortfolioItemPages' => 'PortfolioItemPage'
);
// tidy up the CMS by not showing these fields
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->removeByName("PortfolioItemPages");
return $fields;
}
static $default_sort = "Name ASC";
}
IndustryTag class
<?php
class IndustryTag extends DataObject {
private static $db = array(
'Name' => 'varchar(250)',
);
private static $summary_fields = array(
'Name' => 'Title',
);
private static $field_labels = array(
'Name'
);
private static $belongs_many_many = array(
'PortfolioItemPages' => 'PortfolioItemPage'
);
// tidy up the CMS by not showing these fields
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->removeByName("PortfolioItemPages");
return $fields;
}
static $default_sort = "Name ASC";
}
There needs to be a page for each Portfolio Item, so I made a PortfolioItemPage type, which has 2 tabs: one for Media Type and one for Industry Type. This is so content managers can associated whatever tags they want with each Portfolio Item by checking the appropriate boxes:
PortfolioItemPage.php file:
private static $db = array(
'Excerpt' => 'Text',
);
private static $has_one = array(
'Thumbnail' => 'Image',
'Logo' => 'Image'
);
private static $has_many = array(
'PortfolioChildItems' => 'PortfolioChildItem'
);
private static $many_many = array(
'MediaTypeTags' => 'MediaTypeTag',
'IndustryTags' => 'IndustryTag'
);
public function getCMSFields() {
$fields = parent::getCMSFields();
if ($this->ID) {
$fields->addFieldToTab('Root.Media Type Tags', CheckboxSetField::create(
'MediaTypeTags',
'Media Type Tags',
MediaTypeTag::get()->map()
));
}
if ($this->ID) {
$fields->addFieldToTab('Root.Industry Tags', CheckboxSetField::create(
'IndustryTags',
'Industry Tags',
IndustryTag::get()->map()
));
}
$gridFieldConfig = GridFieldConfig_RecordEditor::create();
$gridFieldConfig->addComponent(new GridFieldBulkImageUpload());
$gridFieldConfig->getComponentByType('GridFieldDataColumns')->setDisplayFields(array(
'EmbedURL' => 'YouTube or SoundCloud Embed Code',
'Thumb' => 'Thumb (135px x 135px)',
));
$gridfield = new GridField(
"ChildItems",
"Child Items",
$this->PortfolioChildItems(),
$gridFieldConfig
);
$fields->addFieldToTab('Root.Child Items', $gridfield);
$fields->addFieldToTab("Root.Main", new TextareaField("Excerpt"), "Content");
$fields->addFieldToTab("Root.Main", new UploadField('Thumbnail', "Thumbnail (400x x 400px)"), "Content");
$fields->addFieldToTab("Root.Main", new UploadField('Logo', "Logo"), "Content");
return $fields;
}
}
class PortfolioItemPage_Controller extends Page_Controller {
private static $allowed_actions = array (
);
public function init() {
parent::init();
}
}
What I thought might be a good approach would be to use jQuery and AJAX to send the ids of the selected facets to the server:
(function($) {
$(document).ready(function() {
var industry = $('.industry');
var media = $('.media');
var tag = $('.tag');
var selectedTags = "";
tag.each(function(e) {
$(this).bind('click', function(e) {
e.preventDefault();
$(this).addClass('selectedTag');
if(selectedTags.indexOf($(this).text()) < 0){
if($(this).hasClass('media')){
selectedTags += + $(this).attr("id") + "," +"media;";
}
else{
selectedTags += + $(this).attr("id") + "," +"industry;";
}
}
sendTag(selectedTags);
}.bind($(this)));
});
function sendTag(TagList){
$.ajax({
type: "POST",
url: "/home/getPortfolioItemsByTags/",
data: { tags: TagList },
dataType: "json"
}).done(function(response) {
var div = $('.portfolioItems');
div.empty();
for (var i=0; i<response.length; i++){
div.append(response[i].name + "<br />");
//return portfolio data here
}
})
.fail(function() {
alert("There was a problem processing the request.");
});
}
});
}(jQuery));
Then on Page.php, I loop through the ids and get the corresponding PortfolioItemPage information based on the facet ids:
public function getPortfolioItemsByTags(){
//remove the last comma from the list of tag ids
$IDs = $this->getRequest()->postVar('tags');
$IDSplit = substr($IDs, 0, -1);
//put the tag ids and their tag names (media or industry) into an array
$IDListPartial = explode(";",$IDSplit);
//This will hold the associative array of ids to types (i.e. 34 => media)
$IDListFinal = array();
array_walk($IDListPartial, function($val, $key) use(&$IDListFinal){
list($key, $value) = explode(',', $val);
$IDListFinal[$key] = $value;
});
//get Portfolio Items based on the tag ids and tag type
foreach($IDListFinal as $x => $x_value) {
if($x_value=='media'){
$tag = MediaTypeTag::get()->byId($x);
$portfolioItems = $tag->PortfolioItemPages();
}
else{
$tag = IndustryTag::get()->byId($x);
$portfolioItems = $tag->PortfolioItemPages();
}
$return = array();
foreach($portfolioItems as $portfolioItem){
$return[] = array(
'thumbnail' => $portfolioItem->Thumbnail()->Link(),
'name' => $portfolioItem->H1,
'logo' => $portfolioItem->Logo()->Link(),
'excerpt' => $portfolioItem->Excerpt,
'id' => $portfolioItem->ID
);
}
return json_encode($return);
}
}
However, this is where I am getting stuck. While I have found some decent examples of building a PHP/MySQL faceted search outside of a CMS, I am not sure what I can modify in order to make the search work inside a CMS. That, and the examples put the facets in one table in the MySQL database whereas I have 2 (As much as I want to have just one MySQL table for both the Media Type and Industry facets, I am not sure if this is a good idea since content managers want to maintain the facet names themselves).
Are there any tutorials out there that might provide further assistance, or possibly a plugin that I have not found yet? If there is a better way to set this faceted search up, by all means, please suggest ideas. This is pretty new to me.

The most efficient way to do this is to filter based on the tag/media type IDs in one query (your example is doing one database query per tag/type, then appending the results).
You should be able to do something like this:
<?php
public function getPortfolioItemsByTags(){
$tagString = $this->getRequest()->postVar('tags');
// remove the last comma from the list of tag ids
$tagString = substr($tagString, 0, -1);
//put the tag ids and their tag names (media or industry) into an array
$tags = explode(";", $tagString);
//This will hold the associative array of ids to types (i.e. 34 => media)
$filters = array(
'media' => array(),
'industry' => array()
);
array_walk($tags, function($val, $key) use(&$filters) {
list($id, $type) = explode(',', $val);
$filters[$type][] = $id;
});
$portfolioItems = PortfolioItemPage::get()->filterAny(array(
'MediaTypeTags.ID' => $filters['media'],
'IndustryTags.ID' => $filters['industry']
));
$return = array();
foreach($portfolioItems as $portfolioItem){
$return[] = array(
'thumbnail' => $portfolioItem->Thumbnail()->Link(),
'name' => $portfolioItem->H1,
'logo' => $portfolioItem->Logo()->Link(),
'excerpt' => $portfolioItem->Excerpt,
'id' => $portfolioItem->ID
);
}
return json_encode($return);
}

Related

Custom WP_Sitemaps_Provider sitemap page loading last blog post instead

I have updated to Wordpress 5.5 and want to remove Yoast from the install as its pretty much only used for sitemaps, however need to create a couple of custom sitemaps based on different post types, which I am currently doing with Yoast.
I am adding a custom provider as seen below which overrides both needed abstract functions. Both of these are working and the new entry is being added to the sitemap index at wp-sitemap.xml
However when clicking on /wp-sitemap-range-1.xml I get resolved with the latest blog post on the site instead of the expected site map with the three post types together.
I cannot find any documentation in the Wordpress API spec or codex yet so am at a bit of a loss at the moment - Any help is appreciated. A link with an example working provider would also be appreciated, as I have searched far to try and find something with no luck.
My next steps to to check all the 404 handlers and rewrite handlers in my theme to see if anything is sending it to the wrong place. I have much more complex sitemaps to produce, but want this one simple aggregation of the three post types to work first.
<?php
//Add provider for post types 'cast', 'keg', 'cider' to create a sitemap called 'range'
add_action('init', function() {
$rangeProvider = new key_sitemapProvider('range', array('cask', 'keg', 'cider'));
wp_register_sitemap_provider('pmrs-range', $rangeProvider);
});
​
/*---------------------*/
class key_sitemapProvider extends WP_Sitemaps_Provider {
public $postTypes = array();
​
/*---------------------*/
public function __construct($name, $postTypes) {
$this->name = $name;
$this->postTypes = $postTypes;
$this->object_type = 'post';
}
​
/*---------------------*/
private function queryArgs(){
return array(
'post_type' => $this->postTypes,
'post_status' => 'publish',
'posts_per_page' => -1,
'orderby' => 'post_date',
'order' => 'DESC'
);
}
​
/*--OVERRIDE-----------*/
public function get_url_list($page_num, $post_type = '') {
$query = new WP_Query($this->queryArgs());
$urlList = array();
​
foreach($query->posts as $post) {
$sitemapEntry = array(
'chf' => 'weekly',
'pri' => 1.0,
'loc' => get_permalink($post),
'mod' => get_the_modified_time('Y-m-d H:i:s', $post)
);
$sitemapEntry = apply_filters('wp_sitemaps_posts_entry', $sitemapEntry, $post, $post_type);
$urlList[] = $sitemapEntry;
}
​
return $urlList;
}
​
/*--OVERRIDE-----------*/
public function get_max_num_pages($post_type = '') {
return 1;
}
/*---------------------*/
}
​
SOLUTION
Thanks to Matt Jaworski's answer below found a way to get this to work, with couple other issues.
Name property of your WP_Sitemaps_Provider extension needs to match the name you are registering with Wordpress. Also this should not contain any special chars
Supported values in the sitemap entries are : changefreq, priority, loc, and lastmod. This is opposed to the shortened version I had before.
Working Example
/*---------------------*/
add_action('init', function() {
$rangeProvider = new key_sitemapProvidor('range', array('cask', 'keg', 'cider'));
wp_register_sitemap_provider('range', $rangeProvider);
});
/*---------------------*/
class key_sitemapProvidor extends WP_Sitemaps_Provider {
public $postTypes = array();
/*---------------------*/
public function __construct($name, $postTypes) {
$this->name = $name;
$this->postTypes = $postTypes;
$this->object_type = 'post';
}
/*---------------------*/
private function queryArgs(){
return array(
'post_type' => $this->postTypes,
'post_status' => 'publish',
'posts_per_page' => -1,
'orderby' => 'post_date',
'order' => 'DESC'
);
}
/*--OVERRIDE-----------*/
public function get_url_list($page_num, $post_type = '') {
$query = new WP_Query($this->queryArgs());
$urlList = array();
foreach($query->posts as $post) {
$sitemapEntry = array(
'changefreq' => 'weekly',
'priority' => 1.0,
'loc' => get_permalink($post),
'lastmod' => get_the_modified_time('Y-m-d H:i:s', $post)
);
$sitemapEntry = apply_filters('wp_sitemaps_posts_entry', $sitemapEntry, $post, $post_type);
$urlList[] = $sitemapEntry;
}
return $urlList;
}
/*--OVERRIDE-----------*/
public function get_max_num_pages($post_type = '') {
return 1;
}
/*---------------------*/
}
I had the same issue, and docs simply don't exist.
Through several trials and errors I figured out most likely WordPress does not like any special chars inside the names.
In my case replacing community-posts with communityposts helped.
Here's the very rough (but working) proof of concept we are working on now:
class PeepSo3_Sitemap_Provider extends WP_Sitemaps_Provider {
private $limit = 10; // #TODO CONFIGURABLE
public function __construct() {
$this->name = 'communityposts';
$this->object_type = 'communityposts';
}
private function sql($page_num) {
$sql =""; // your queries here;
return $wpdb->get_results($sql);
}
// retrieve a page of results
public function get_url_list( $page_num, $object_subtype = '' ) {
$url_list = [];
$posts = $this->sql($page_num);
foreach($posts as $post) {
$url_list[] = ['loc' => $post->url; // depends on your item structure
}
return $url_list;
}
// estimate how many pages are available
public function get_max_num_pages( $object_subtype = '' ) {
$posts = $this->sql(-1);
return ceil($posts[0]->count_posts/$this->limit);
}
}
// Register XML Sitemap Provider
add_filter('init', function() {
$provider = new PeepSo3_Sitemap_Provider();
wp_register_sitemap_provider( 'communityposts', $provider );
});

how do i add an array of objects or an extra field to a wordpress json api core controller?

Is there a way to add an array of objects to a core controller of the json apiplugin
the core controller i want to modify is 'get_category_index' and it returns these objects
id
post_count
name
slug
what i want to do is an extra object, 'posts' which lists the posts that are assigned to that category
the code currently looks like this in jsonapi/controllers/core.php for that specific controller looks like that
public function get_category_index() {
global $json_api;
$args = null;
if (!empty($json_api->query->parent)) {
$args = array(
'parent' => $json_api->query->parent
);
}
$categories = $json_api->introspector->get_categories($args);
return array(
'count' => count($categories),
'categories' => $categories
);
}
and i thought something like this would do the trick
public function get_category_index() {
global $json_api;
$args = null;
if (!empty($json_api->query->parent)) {
$args = array(
'parent' => $json_api->query->parent
);
}
$categories = $json_api->introspector->get_categories($args);
return array(
'count' => count($categories),
'categories' => $categories,
'posts' => $posts,
);
}
but i'm obviously im missing something, how can i add that array of posts?
i found the category.php which looks like this
class JSON_API_Category {
var $id; // Integer
var $slug; // String
var $title; // String
var $description; // String
var $parent; // Integer
var $post_count; // Integer
var $posts; // array
function JSON_API_Category($wp_category = null) {
if ($wp_category) {
$this->import_wp_object($wp_category);
}
}
function import_wp_object($wp_category) {
$this->id = (int) $wp_category->term_id;
$this->slug = $wp_category->slug;
$this->title = $wp_category->name;
$this->description = $wp_category->description;
$this->parent = (int) $wp_category->parent;
$this->post_count = (int) $wp_category->count;
$this->posts = $wp_category->posts;
}
}
where i added the
var $posts; // array
and called it
$this->posts = $wp_category->posts;
however obviously i'm getting null on posts in my json return, whereas i need it to return the posts in that category.

Trying to remove/hide fields in custom tab in SilverStripe

I am trying to figure out a way (if possible) to remove or hide certain fields in a custom tab. The custom tab is labeled "Rotator" and it holds images that can be used for a rotating banner on a page. The home page banner is a little different in that it has 2 extra fields that aren't needed on the subpages: BackgroundImage and Body(which is meant to hold a variety of text). I want to make things simple for the content manager, so I want to hide these fields on the subpages.
I am aware of removeFieldFromTab and how it works, and I was thinking of using it on the Page.php file (since that is basically the main template for all page types in my SilverStripe file):
public function getCMSFields() {
$fields = parent::getCMSFields();
$gridFieldConfig = GridFieldConfig_RecordEditor::create();
$gridFieldConfig->addComponent(new GridFieldBulkImageUpload());
$gridFieldConfig->addComponent(new GridFieldSortableRows('SortOrder'));
$gridFieldConfig->getComponentByType('GridFieldDataColumns')->setDisplayFields(array(
// field from drawer class => label in UI
'ID' => 'ID',
'Title' => 'Title',
'Thumbnail' => 'Thumbnail',
'InternalURL.Link' => 'Internal URL',
));
$gridfield = new GridField(
"Rotator",
"Rotator",
$this->Rotator()->sort("SortOrder"),
$gridFieldConfig
);
$fields->addFieldToTab('Root.Rotator', $gridfield);
$fields->addFieldToTab("Root.Main", new TextField("H1"), "Content");
$fields->addFieldToTab("Root.Main", new TextField("Subheader"), "Content");
$fields->addFieldToTab('Root.Main', new TextField('PageTitle', 'Page Title'), 'MetaDescription');
$fields->removeFieldFromTab('Root.Rotator', 'Body');
$fields->removeFieldFromTab('Root.Rotator', 'BackgroundImage');
return $fields;
}
Here is the code for the Rotator class:
<?php
class RotatorImage extends DataObject {
public static $db = array(
'SortOrder' => 'Int',
'Header' => 'varchar',
'Body' => 'HTMLText',
);
// One-to-one relationship with gallery page
public static $has_one = array(
'Image' => 'Image',
'BackgroundImage' => 'Image',
'Page' => 'Page',
'InternalURL' => 'SiteTree',
);
// tidy up the CMS by not showing these fields
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->removeFieldFromTab("Root.Main","PageID");
$fields->removeFieldFromTab("Root.Main","SortOrder");
return $fields;
}
// Tell the datagrid what fields to show in the table
public static $summary_fields = array(
'ID' => 'ID',
'Title' => 'Title',
'Thumbnail' => 'Thumbnail',
'InternalURLID' => 'Internal URL',
);
// this function creates the thumnail for the summary fields to use
public function getThumbnail() {
return $this->Image()->CMSThumbnail();
}
public function canEdit() {
return true;
}
public function canDelete() {
return true;
}
public function canCreate(){
return true;
}
public function canPublish(){
return true;
}
public function canView(){
return true;
}
}
However this does not work, and I am sure that I have the fields names correct. I tried 'Root.Rotator.Main' and 'Root.Rotator.Content' just to see what would happen and those also did not work. What am I missing? Is it possible to hide fields on a custom tab this way, or do I need to try something else?
well, you want to hide the fields in the gridfield detail form? that cannot be done in your pages getCMSFields(), as the grid is responsible for generating the detail form. Two possible solutions:
1) tell the grid to hide that fields with a custom component. I dunno how to do it
2) tell your Rotator class to show the fields ONLY if the related page is a homepage:
public function getCMSFields() {
$fields = parent::getCMSFields();
//...other stuff....
$isOnHomePage = ($this->Page() && $this->Page()->ClassName == 'HomePage'); //put in your own classname or conditions
if(!$isOnHomePage) {
//remove the fields if you're not on the HomePage
$fields->removeByName('Body');
//we need to suffix with "ID" when we have a has_one relation!
$fields->removeByName('BackGroundImageID');
}
return $fields;
}
This will work...
$fields->removeByName('FieldName');

Magento ProductController append categoryId to products

I am adding a mass action to add a category. I am most of the way there I only have one function left to figure out.
Clr\Categorymassaction\controllers\Adminhtml\Catalog\ProductController.php
class Clr_Categorymassaction_Adminhtml_Catalog_ProductController extends Mage_Adminhtml_Controller_Action
{
public function massCategoryAction()
{
$productIds = $this->getRequest()->getParam('product');
$cat = $this->getRequest()->getParam('Category');
if (!is_array($productIds)) {
$this->_getSession()->addError($this->__('Please select product(s).'));
$this->_redirect('*/*/index');
}
else {
$cat = $category['label']->getCategoryId();
foreach($productIds as $product) {
//Process $cat into categoryId append categoryId to $productId
$cat->setPostedProducts($product);
}
//Save product
$cat->save();
}
}
}
Clr\Categorymassaction\Model\Observer
class Clr_Categorymassaction_Model_Observer {
public function addCategoryMassAction(Varien_Event_Observer $observer)
{
$block = $observer ->getBlock();
if ($block instanceof Mage_Adminhtml_Block_Catalog_Product_Grid) {
$block->getMassactionBlock()->addItem('Clr_Categorymassaction', array(
'label' => Mage::helper('catalog')->__('Add to Category'),
'url' => $block->getUrl('*/*/massCategory', array('_current' => true)),
'additional'=> array(
'visibility' => array(
'name' =>'Category',
'class' =>'required-entry',
'label' =>Mage::helper('catalog')->__('Categories'),
'type' => 'select',
'values' => Mage::getModel('Categorymassaction/system_config_source_category')->toOptionArray(),
'renderer' => 'Categorymassaction/catalog_product_grid_render_category',
)
)
));
};
}
}
One last thing
class Clr_Categorymassaction_Model_System_Config_Source_Category
{
public function toOptionArray($addEmpty = true)
{
$options = array();
foreach ($this->load_tree() as $category) {
$options[$category['value']] = $category['label'];
}
return $options;
}
I am mostly in trouble here because I am refactoring, Flagbit_changeattributeset and Vuleticd_AdminGridCategoryFilter. I know what I need to do (at least I think I do) I just don't know how to finish this off. Thanks for your eyes and ears if you read it all.
UPDATE: The observer from Vuleticd_AdminGridCategoryFilter had this additional code
'filter_condition_callback' => array($this, 'filterCallback'),
)
)
));
};
}
public function filterCallback($collection, $column)
{
$value = $column->getFilter()->getValue();
$_category = Mage::getModel('catalog/category')->load($value);
$collection->addCategoryFilter($_category);
return $collection;
}
This was used to apply the filter to the grid. What I am trying to do is instead of using the dropdown to filter column fields; use the dropdown to trigger the ProductController to pass the selected items a new categoryid.
https://magento.stackexchange.com/questions/67234/productcontroller-for-mass-action Asked this question over at magento's stackexchange figured I would post the link here for posterity.

Add Button on Custom Plugin in WordPress

I am working on custom plugin in WP.I have showed the data in table format.Now i am trying to add functionality to plugin.But my table missing header and footer just like normal data listing in WP like Pages and Posts etc.Also i added the new button link.Either this is the right way ? I want to load my page on button clickMy code is :
function extra_tablenav( $which ) {
if ( $which == "top" ){
//The code that goes before the table is here
echo '<h2>Letter Templates Add New</h2>';
}
}
function get_columns() {
return $columns= array(
'col_id'=>__('ID'),
'col_name'=>__('Name'),
'col_url'=>__('Url'),
'col_description'=>__('Description')
);
}
public function get_sortable_columns() {
return $sortable = array(
'col_id'=>'id',
'col_name'=>'name'
);
}
You can use the $_column_headers extended Property
this property is assigned automatically, must manually define it in their prepare_items() or __construct() methods.
like,
function prepare_items(){
$columns = array(
'cb' => '<input type="checkbox" />', //Render a checkbox instead of text
'col_id' => 'ID',
'col_name' => 'Name',
'col_url' => 'URL',
'col_description' => 'Description',
);
$sortable_columns = array(
'col_id' => 'ID',
'col_name' => 'Name',
);
$hidden = array();
$this->_column_headers = array($columns, $hidden, $sortable);
}
function extra_tablenav( $which ) {
if ( $which == "top" ){
echo '<h2>Letter Templates Add New</h2>';
}
}
The Wordpress natively supports URLs like wp-admin/admin.php?page= you acess plugin pages like wp-admin/admin.php?page=mypage&tab=add-letter
And then in your code you just look at the GET and pull up the main page or a sub-page as needed.
like
if(isset($_GET['type']) && $_GET['type']=='new'){
include('add-letter.php');
}
check

Categories