I'm trying to do something very different for a SilverStripe site: on several subpages are tables of data, and these tables each have their own set of column headers, and some tables have more columns than others. I want to avoid building out tables in the Rich Text Editor as that is prone to a lot of mistakes and it's a hassle to maintain over time.
What I would like to do is create a DataObject that allows for a nth number of columns and an nth number of corresponding rows. This way I can call a loop (or possibly two) inside the template where I have the HTML table structure already in place. The content managers have full control over which columns are in the tables for any give subpage, and they don't have to worry about maintaining the HTML table setup.
I've had a couple of ideas that don't produce the results I want without a) making the UI experience too complex for content managers and b) not being able to properly link the columns with the rows.
I have thought of creating a DataObject for Table Headers and one for Table Rows, but then I'm stumped on how to combine them in such a way that would make sense, especially since there could be any number of columns.
Would anyone have any suggestions on to approach this?
UPDATE: Ok, I have something going for the TableRowItem data object that may work, and is close to working. However, the issue is this now: How do I save the field values to the database when I am creating them basically on the fly? As it is now, the only field that saves to the database is the PDF file upload field, everything else is erased upon hitting "create."
<?php
class TruckBodyPdfTableRowItem extends DataObject {
private static $db = array(
);
// One-to-one relationship with gallery page
private static $has_one = array(
'TablePage'=> 'Page',
'TableColumnSet' => 'TableColumnSet',
'PDF' => 'File',
);
// tidy up the CMS by not showing these fields
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->removeFieldFromTab("Root.Main","TablePageID");
$fields->removeFieldFromTab("Root.Main","TableColumnSetID");
$fields->removeFieldFromTab("Root.Main","SortOrder");
$fields->addFieldsToTab("Root.Main", $this->getMyColumnOptions());
return $fields;
}
public function getMyColumnOptions()
{
$columnArray = [];
$Columns = DataObject::get('TableColumnSet');
foreach($Columns as $Column){
$columnArray[] = TextField::create($Column->TableColumnHeader);
}
return $columnArray;
}
// Tell the datagrid what fields to show in the table
private static $summary_fields = array(
);
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;
}
}
But those are the tricky parts: Figuring out how to map values from one DataObject into labels for another, and then auto-generating an nth number of rows based on how many columns have been created.
<?php
class TablePage extends Page
{
private static $db = array(
'H1' => 'varchar(250)',
);
private static $has_many = array(
'TableRowItems' => 'TableRowItem',
'TableColumnSets' => 'TableColumnSet'
);
private static $has_one = array(
);
public function getCMSFields()
{
$fields = parent::getCMSFields();
$fields->addFieldToTab("Root.Main", new TextField("H1"), "Content");
$gridFieldConfig = GridFieldConfig_RecordEditor::create();
$gridFieldConfig->getComponentByType('GridFieldDataColumns')->setDisplayFields(array(
// field from drawer class => label in UI
'TableColumnHeader' => 'Table Column Header'
));
$gridfield = new GridField(
"TableColumnSets",
"Table Column Sets",
$this->TableColumnSets(),
$gridFieldConfig
);
$fields->addFieldToTab('Root.Specs Table', $gridfield);
$gridFieldConfig2 = GridFieldConfig_RecordEditor::create();
$gridFieldConfig2->getComponentByType('GridFieldDataColumns')->setDisplayFields(array(
// field from drawer class => label in UI
'TableRowValue' => 'Table Row Value'
));
$gridfield2 = new GridField(
"TableRowItems",
"Table Row Items",
$this->TableRowItems(),
$gridFieldConfig2
);
$fields->addFieldToTab('Root.Specs Table', $gridfield2);
return $fields;
}
}
class TablePage_Controller extends Page_Controller
{
private static $allowed_actions = array(
);
public function init()
{
parent::init();
// You can include any CSS or JS required by your project here.
// See: http://doc.silverstripe.org/framework/en/reference/requirements
}
}
Here are the classes TableColumnSet and TableRowValue. I figured, there would be one set of column headers associated with an nth number of rows, so I figured there would be a $has_many relationship between the two classes, in that a TableColumnSet could have many TableRowValues, but there would only be one TableColumnSet for all the TableRowValues. I was hoping to associate the TableRowValues to the TableColumnSet values using a dropdown with all the column headers created but that just sounds like a bad idea. Having to manually associate every field in a row to the column headers seems tedious and potentially difficult content managers.
<?php
class TableColumnSet extends DataObject {
private static $db = array(
'SortOrder' => 'Int',
'TableColumnHeader'=>'varchar(250)'
);
// One-to-one relationship with gallery page
private static $has_one = array(
'TablePage'=> 'Page'
);
private static $has_many = array(
'TableRowItems' => 'TableRowItem'
);
// tidy up the CMS by not showing these fields
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->removeFieldFromTab("Root.Main","TablePageID");
$fields->removeFieldFromTab("Root.Main","SortOrder");
return $fields;
}
// Tell the datagrid what fields to show in the table
private static $summary_fields = array(
'TableColumnHeader' => 'Table Column Header',
);
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;
}
}
I feel like may be on to something here, at least in regards to the relationship between the column headers and rows? I'm not sure, though.
I might be off base here, since I have no experience with SilverStripe. But... my PHP / HTML table solution might apply here:
<?php
// parse your table data into this structure
$tableData = array(
"rowOne" => array(
"columnName" => "columnValue1",
"colName" => "colValue1"
// .....
),
"rowTwo" => array(
"columnName" => "columnValue2",
"colName" => "colValue2"
// .....
)
);
// now loop through the array with a printHeader parameter
$tableHTML = array(
"<table>"
);
$tableHead = array(
"<thead>"
);
$tableBody = array(
"<tbody>"
);
$printHeader = true;
foreach ($tableData as $row) {
foreach ($row as $column => $value) {
$tableRow = "<tr>";
if ($printHeader) {
$tableHead[] = "<th>".$column."</th>";
}
$tableRow .= "<td>".$value."</td>";
}
$tableBody[] = $tableRow."</tr>";
// after the first row, set printHeader to false and close the <thead>
$printHeader = false;
$tableHead[] = "</thead>";
}
// implode table header to string with linebreaks
$tableHead = implode(PHP_EOL, $tableHead);
// close table <tbody> & implode to string with linebreaks
$tableBody[] = "</tbody>";
$tableBody = implode(PHP_EOL, $tableBody);
// add all table elements together
$tableHTML[] = $tableHead;
$tableHTML[] = $tableBody;
$tableHTML[] = "</table>";
// implode table array to string
$tableHTML = implode(PHP_EOL, $tableHTML);
// print or write anywhere
echo($tableHTML);
?>
The array structure for all the steps in the loop are to keep the default server memory cleaner to remove old data. If you concat ($var .= "string";) everything as strings all the references will stay stored in memory and bog down the server when displaying large tables.
I hope this is of some help
Related
How can I create a production collection based on pricerange in Magento 2.
This is what i have so far:
<?php namespace Qxs\Related\Block;
class Related extends \Magento\Framework\View\Element\Template
{
protected $_productCollectionFactory;
public function __construct(
\Magento\Backend\Block\Template\Context $context,
\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory,
\Magento\Catalog\Model\Product\Attribute\Source\Status $productStatus,
\Magento\Catalog\Model\Product\Visibility $productVisibility,
array $data = []
)
{
$this->_productCollectionFactory = $productCollectionFactory;
$this->productStatus = $productStatus;
$this->productVisibility = $productVisibility;
parent::__construct($context, $data);
}
public function getProductCollection()
{
//var_dump($this->currentProduct());
$collection = $this->_productCollectionFactory->create();
$collection->addAttributeToSelect('*')
->addAttributeToFilter('special_price', ['from' => 0, 'to' => 1000])
->addAttributeToFilter('status', ['in' => $this->productStatus->getVisibleStatusIds()])
->setVisibility($this->productVisibility->getVisibleInSiteIds())
->setPageSize(5);
return $collection;
}
public function currentProduct()
{
return $this->_coreRegistry->registry('product');
}
}
?>
However, the code does not return a result including a price range. The result is totally empty but should return some products, how can I filter on price-range?
Thanks,
Range filters, to me work in this way, with addFieldToFilter. Have you tried it?
$orders = $this->_orderCollectionFactory->create()
->addAttributeToSelect('*')
->addFieldToFilter( 'created_at' , array('from' => $dateFrom, 'to' => $dateTo) )
->setOrder('created_at', 'desc' );
->setPageSize(200);
I am having trouble setting the SortOrder of a nested has_many relationship where the child items are drag and drop reorder-able though the Orderable Rows module of Gridfield Extensions. I am also using the Multi Class Editor
I have a Page that has many ComplexStrips. These ComplexStrip objects have many 'LinkedObjects'.
The issue:
When I try to add a ComplexStrip via the GridField I get the following error: Uncaught InvalidArgumentException: Invalid sort() column
My Setup:
Page Class:
This class is working fine when I browse to edit it in the CMS.
<?php
class Page extends SiteTree {
private static $has_many = array(
'ComplexStrips' => 'ComplexStrip'
);
public function getCMSFields() {
$fields = parent::getCMSFields();
$dataColumns = new GridFieldDataColumns();
$dataColumns->setDisplayFields(
array(
'Title' => 'Title',
'ClassName' => 'Class Name'
)
);
$multiClassConfig = new GridFieldAddNewMultiClass();
$multiClassConfig->setClasses(
array(
'ComplexStrip'
)
);
$stripsGridField = GridField::create('ComplexStrips', 'Complex strips', $this->Strips(),
GridFieldConfig::create()->addComponents(
new GridFieldToolbarHeader(),
new GridFieldDetailForm(),
new GridFieldEditButton(),
new GridFieldDeleteAction('unlinkrelation'),
new GridFieldDeleteAction(),
new GridFieldOrderableRows('SortOrder'), // This line causes the issue
new GridFieldTitleHeader(),
$multiClassConfig,
$dataColumns
)
);
$fields->addFieldsToTab(
'Root.Strips',
array(
$stripsGridField
)
);
return $fields;
}
}
TextBlocksStrip Class:
As soon as I try to add one of these via the gridfield on Page I get the error: Uncaught InvalidArgumentException: Invalid sort() column
<?php
class ComplexStrip extends DataObject {
private static $db = array(
'SortOrder' => 'Int'
);
private static $has_many = array(
'TextBlocks' => 'TextBlock'
);
//-------------------------------------------- CMS Fields
public function getCMSFields() {
$fields = parent::getCMSFields();
//---------------------- Main Tab
$textBlocks = GridField::create('TextBlocks', 'Text blocks', $this->TextBlocks(),
GridFieldConfig::create()->addComponents(
new GridFieldToolbarHeader(),
new GridFieldAddNewButton('toolbar-header-right'),
new GridFieldDetailForm(),
new GridFieldDataColumns(),
new GridFieldEditButton(),
new GridFieldDeleteAction('unlinkrelation'),
new GridFieldDeleteAction(),
new GridFieldOrderableRows('SortOrder'),
new GridFieldTitleHeader(),
new GridFieldAddExistingAutocompleter('before', array('Title'))
)
);
$fields->addFieldsToTab('Root.Main',
array(
$textBlocks
)
);
return $fields;
}
}
TextBlock Class:
<?php
class TextBlock extends DataObject {
private static $db = array(
'SortOrder' => 'Int'
);
private static $default_sort = 'Sort';
private static $has_one = array(
'TextBlocksStrip' => 'TextBlocksStrip'
);
}
If I remove the line new GridFieldOrderableRows('SortOrder') all works but is obviously not reorder-able.
I am pulling my hair out over this one. Makes no sense to me why this isn't working.
I have made a fairly elegant workaround.
The SortOrder could not be queried as the parent Object ComplexStrip doesn't exist at the time it is attempting to join the sorted table. So wrapping the GridFieldOrderableRows in an if to check if the parent object has been assigned an ID yet.
// Need to wait until parent object is created before trying to set sort order
if($this->ID) {
$textBlocks->addComponents(
new GridFieldOrderableRows('Sort')
);
}
After that everything works the same as before in the CMS. I will open an issue on the module now to see if there is a more permanent solution.
I had the same error as I was using $many_many instead of $has_many. I can see you used $has_many so wouldn't solve your issue but might help others encountering the same error message like I was.
I'm following this tutorial
I have a page called 'Contact.ss'. The php file looks like this:
class Contact extends Page {
private static $has_one = array (
'Photograph' => 'Image'
);
static $db = array (
'MailTo' => 'Varchar(100)',
'SubmitText' => 'Text'
);
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.Main', $Photograph = UploadField::create('Photograph'), 'Content');
$Photograph->getValidator()->setAllowedExtensions(array('png','jpeg','jpg','gif'));
$Photograph->setFolderName('photographs');
$fields->addFieldToTab("Root.Main", new Textfield('Mailto', 'Address to email contact form submission to'));
$fields->addFieldToTab("Root.Main", new TextareaField('SubmiteText', 'Text displayed after contact form submission'));
return $fields;
}
}
class Contact_Controller extends Page_Controller {
static $allowed_actions = array(
'ContactForm'
);
function ContactForm() {
// Create fields
$fields = new FieldSet(
new TextField('Name', 'Name*'),
new EmailField('Email', 'Email*'),
new TextareaField('Comments','Comments*')
);
// Create action
$actions = new FieldSet(
new FormAction('SendContactForm', 'Send')
);
// Create Validators
$validator = new RequiredFields('Name', 'Email', 'Comments');
return new Form($this, 'ContactForm', $fields, $actions, $validator);
}
}
But when I call $ContactForm in the template I get a blank screen when I try to load the page. (500 error)
I've checked to see if it's possible to call the function from the template by replacing all the ContactForm()'s code with:
return "Hello, World!"
It works, so I know the function is being called. But I can't see what's wrong with the code from the tutorial.
Can anyone help me out?
The issue is the tutorial you have used is written for SilverStripe 2.4 while you are using a newer version, SilverStripe 3.1.
For SilverStripe 3.1 I suggest going through the SilverStripe Frontend Forms lesson rather than the SSBits tutorial. The SSBits tutorial is from 2010 and is for SilverStripe 2.4. The SilverStripe Frontend Forms lesson is from 2015 and is for the current version of SilverStripe.
With your current code there are a number of bits of code that need to be updated to work in the latest version of SilverStripe.
FieldSet has been replaced by FieldList. You will need to replace each instance of FieldSet with FieldList in your code.
Your ContactForm should look more like this:
function ContactForm() {
// Create fields
$fields = FieldList::create(
TextField::create('Name', 'Name*'),
EmailField::create('Email', 'Email*'),
TextareaField::create('Comments','Comments*')
);
// Create action
$actions = FieldList::create(
FormAction::create('SendContactForm', 'Send')
);
// Create Validators
$validator = RequiredFields::create('Name', 'Email', 'Comments');
return Form::create($this, 'ContactForm', $fields, $actions, $validator);
}
In SilverStripe 3.1 the in built static variables need to be declared private.
Make sure you declare your $allowed_actions as private:
private static $allowed_actions = array(
'ContactForm'
);
As well as your $db as private:
private static $db = array (
'MailTo' => 'Varchar(100)',
'SubmitText' => 'Text'
);
I have a class called Recipe as follows:
class Recipe extends Page {
static $db = array(
"Title" => "Text"
);
static $many_many = array(
"RecipeTypes" => "RecipeType"
);
}
And a class called RecipeType as follows:
class RecipeType extends DataObject {
static $db = array(
"Title" => "Text",
);
static $belongs_many_many = array(
"Recipes" => "Recipe"
);
}
For my site's search functionality, I want the search results to be every Recipe which has a Title that matches the search parameter, as well as every Recipe which has a RecipeType whose Title matches the search parameter.
I've tried the following:
return Recipe::get()->filterAny(array(
'Title:PartialMatch' => $searchString,
'RecipeTypes.Title:PartialMatch' => $searchString
));
But that is only returning Recipes whose Title matches the search parameter; it's not returning any Recipes where one of the Recipe's RecipeTypes' Title property matches the search parameter.
Any ideas?
Managed to get it working with the following:
$recipes_from_title = Recipe::get()->filter('Title:PartialMatch', $searchString);
$recipes_from_diet = RecipeType::get()->filter('Title:PartialMatch', $searchString)->relation('Recipes');
$matching_recipes = new ArrayList();
foreach($recipes_from_title as $recipe_from_title) {
$matching_recipes->push($recipe_from_title);
}
foreach($recipes_from_diet as $recipe_from_diet) {
$matching_recipes->push($recipe_from_diet);
}
return $matching_recipes;
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.